// 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.Linq; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// /// Represents a delegating embedding generator that caches the results of embedding generation calls, /// storing them as JSON in an . /// /// The type from which embeddings will be generated. /// The type of embeddings to generate. /// /// The provided implementation of is thread-safe for concurrent /// use so long as the employed is similarly thread-safe for concurrent use. /// 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. /// The underlying . /// A instance that will be used as the backing store for the cache. /// is . public DistributedCachingEmbeddingGenerator(IEmbeddingGenerator innerGenerator, IDistributedCache storage) : base(innerGenerator) { _ = Throw.IfNull(storage); _storage = storage; _jsonSerializerOptions = AIJsonUtilities.DefaultOptions; } /// Gets or sets JSON serialization options to use when serializing cache data. /// is . public JsonSerializerOptions JsonSerializerOptions { get => _jsonSerializerOptions; set { _ = Throw.IfNull(value); _jsonSerializerOptions = 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) { _ = Throw.IfNull(key); _jsonSerializerOptions.MakeReadOnly(); if (await _storage.GetAsync(key, cancellationToken) is byte[] existingJson) { return JsonSerializer.Deserialize(existingJson, (JsonTypeInfo)_jsonSerializerOptions.GetTypeInfo(typeof(TEmbedding))); } return null; } /// protected override async Task WriteCacheAsync(string key, TEmbedding value, CancellationToken cancellationToken) { _ = Throw.IfNull(key); _ = Throw.IfNull(value); _jsonSerializerOptions.MakeReadOnly(); var newJson = JsonSerializer.SerializeToUtf8Bytes(value, (JsonTypeInfo)_jsonSerializerOptions.GetTypeInfo(typeof(TEmbedding))); await _storage.SetAsync(key, newJson, cancellationToken); } /// Computes a cache key for the specified values. /// The values to inform the key. /// The computed key. /// /// /// The are serialized to JSON using in order to compute the key. /// /// /// The generated cache key is not guaranteed to be stable across releases of the library. /// /// 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); } } }