diff --git a/dotnet/src/Connectors/Connectors.Memory.Pinecone/Http/SecureHttpHandler.cs b/dotnet/src/Connectors/Connectors.Memory.Pinecone/Http/SecureHttpHandler.cs deleted file mode 100644 index a983e44637d0..000000000000 --- a/dotnet/src/Connectors/Connectors.Memory.Pinecone/Http/SecureHttpHandler.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Net.Http; - -namespace Microsoft.SemanticKernel.Connectors.Memory.Pinecone.Http; - -internal static class HttpHandlers -{ - public static HttpClientHandler CheckCertificateRevocation { get; } = new() - { - CheckCertificateRevocationList = false, - AllowAutoRedirect = true - }; -} diff --git a/dotnet/src/Connectors/Connectors.Memory.Pinecone/IPineconeClient.cs b/dotnet/src/Connectors/Connectors.Memory.Pinecone/IPineconeClient.cs index a78f0f8c6e86..ea8e7eeca6fb 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Pinecone/IPineconeClient.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Pinecone/IPineconeClient.cs @@ -21,7 +21,7 @@ public interface IPineconeClient /// Whether to include the vector values /// The cancellation token /// A list of vector records - public IAsyncEnumerable FetchVectorsAsync( + IAsyncEnumerable FetchVectorsAsync( string indexName, IEnumerable ids, string indexNamespace = "", @@ -38,7 +38,7 @@ public interface IPineconeClient /// whether to include the metadata /// /// a list of query matches - public IAsyncEnumerable QueryAsync( + IAsyncEnumerable QueryAsync( string indexName, Query query, bool includeValues = false, @@ -57,7 +57,7 @@ public interface IPineconeClient /// The name assigned to a collection of vectors. /// A filter to apply to the results /// Cancellation token. - public IAsyncEnumerable<(PineconeDocument, double)> GetMostRelevantAsync( + IAsyncEnumerable<(PineconeDocument, double)> GetMostRelevantAsync( string indexName, IEnumerable vector, double threshold, @@ -160,7 +160,7 @@ Task UpdateAsync( /// /// The name assigned to a collection of vectors. /// Cancellation Token. - public Task DoesIndexExistAsync(string indexName, CancellationToken cancellationToken = default); + Task DoesIndexExistAsync(string indexName, CancellationToken cancellationToken = default); /// /// Describe index diff --git a/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeClient.cs b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeClient.cs index c52a0f86fa0a..f5fdf2192d0c 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeClient.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeClient.cs @@ -12,7 +12,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Connectors.Memory.Pinecone.Http; using Microsoft.SemanticKernel.Connectors.Memory.Pinecone.Http.ApiSchema; using Microsoft.SemanticKernel.Connectors.Memory.Pinecone.Model; @@ -21,21 +20,22 @@ namespace Microsoft.SemanticKernel.Connectors.Memory.Pinecone; /// /// A client for the Pinecone API /// -public sealed class PineconeClient : IPineconeClient, IDisposable +public sealed class PineconeClient : IPineconeClient { /// /// Initializes a new instance of the class. /// - /// - /// - /// - public PineconeClient(string pineconeEnvironment, string apiKey, ILogger? logger = null) + /// The environment for Pinecone. + /// The API key for accessing Pinecone services. + /// An optional logger instance for logging. + /// An optional HttpClient instance for making HTTP requests. + public PineconeClient(string pineconeEnvironment, string apiKey, ILogger? logger = null, HttpClient? httpClient = null) { this._pineconeEnvironment = pineconeEnvironment; this._authHeader = new KeyValuePair("Api-Key", apiKey); this._jsonSerializerOptions = PineconeUtils.DefaultSerializerOptions; this._logger = logger ?? NullLogger.Instance; - this._httpClient = new HttpClient(HttpHandlers.CheckCertificateRevocation); + this._httpClient = httpClient ?? new HttpClient(NonDisposableHttpClientHandler.Instance, disposeHandler: false); this._indexHostMapping = new ConcurrentDictionary(); } @@ -523,12 +523,6 @@ public async Task ConfigureIndexAsync(string indexName, int replicas = 1, PodTyp this._logger.LogDebug("Collection created. {0}", indexName); } - /// - public void Dispose() - { - this._httpClient.Dispose(); - } - #region private ================================================================================ private readonly string _pineconeEnvironment; diff --git a/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeKernelBuilderExtensions.cs new file mode 100644 index 000000000000..e43b84ef56b8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeKernelBuilderExtensions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.Memory.Pinecone; + +#pragma warning disable IDE0130 +namespace Microsoft.SemanticKernel; +#pragma warning restore IDE0130 + +/// +/// Provides extension methods for the class to configure Pinecone connectors. +/// +public static class PineconeKernelBuilderExtensions +{ + /// + /// Registers Pinecone Memory Store. + /// + /// The instance + /// The environment for Pinecone. + /// The API key for accessing Pinecone services. + /// An optional HttpClient instance for making HTTP requests. + /// Self instance + public static KernelBuilder WithPineconeMemoryStore(this KernelBuilder builder, + string environment, + string apiKey, + HttpClient? httpClient = null) + { + builder.WithMemoryStorage((parameters) => + { + var client = new PineconeClient( + environment, + apiKey, + parameters.Logger, + GetHttpClient(parameters.Config, httpClient, parameters.Logger)); + + return new PineconeMemoryStore(client, parameters.Logger); + }); + + return builder; + } + + /// + /// Retrieves an instance of HttpClient. + /// + /// The kernel configuration. + /// An optional pre-existing instance of HttpClient. + /// An optional logger. + /// An instance of HttpClient. + private static HttpClient GetHttpClient(KernelConfig config, HttpClient? httpClient, ILogger? logger) + { + if (httpClient == null) + { + var retryHandler = config.HttpHandlerFactory.Create(logger); + retryHandler.InnerHandler = NonDisposableHttpClientHandler.Instance; + return new HttpClient(retryHandler, false); // We should refrain from disposing the underlying SK default HttpClient handler as it would impact other HTTP clients that utilize the same handler. + } + + return httpClient; + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Memory/Pinecone/PineconeKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/Memory/Pinecone/PineconeKernelBuilderExtensionsTests.cs new file mode 100644 index 000000000000..2fc1abd2d1e7 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/Memory/Pinecone/PineconeKernelBuilderExtensionsTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.Memory.Pinecone; + +public sealed class PineconeKernelBuilderExtensionsTests : IDisposable +{ + private HttpMessageHandlerStub messageHandlerStub; + private HttpClient httpClient; + + public PineconeKernelBuilderExtensionsTests() + { + this.messageHandlerStub = new HttpMessageHandlerStub(); + + this.httpClient = new HttpClient(this.messageHandlerStub, false); + } + + [Fact] + public async Task PineconeMemoryStoreShouldBeProperlyInitialized() + { + //Arrange + this.messageHandlerStub.ResponseToReturn.Content = new StringContent("[\"fake-index1\"]", Encoding.UTF8, MediaTypeNames.Application.Json); + + var builder = new KernelBuilder(); + builder.WithPineconeMemoryStore("fake-environment", "fake-api-key", this.httpClient); + builder.WithAzureTextEmbeddingGenerationService("fake-deployment-name", "https://fake-random-test-host/fake-path", "fake -api-key"); + var kernel = builder.Build(); //This call triggers the internal factory registered by WithPineconeMemoryStore method to create an instance of the PineconeMemoryStore class. + + //Act + await kernel.Memory.GetCollectionsAsync(); //This call triggers a subsequent call to Pinecone memory store. + + //Assert + Assert.Equal("https://controller.fake-environment.pinecone.io/databases", this.messageHandlerStub?.RequestUri?.AbsoluteUri); + + var headerValues = Enumerable.Empty(); + var headerExists = this.messageHandlerStub?.RequestHeaders?.TryGetValues("Api-Key", out headerValues); + Assert.True(headerExists); + Assert.Contains(headerValues!, (value) => value == "fake-api-key"); + } + + public void Dispose() + { + this.httpClient.Dispose(); + this.messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/SemanticKernel/KernelBuilder.cs b/dotnet/src/SemanticKernel/KernelBuilder.cs index 8ef434707dce..929b3df4421f 100644 --- a/dotnet/src/SemanticKernel/KernelBuilder.cs +++ b/dotnet/src/SemanticKernel/KernelBuilder.cs @@ -23,7 +23,7 @@ public sealed class KernelBuilder private KernelConfig _config = new(); private ISemanticTextMemory _memory = NullMemory.Instance; private ILogger _logger = NullLogger.Instance; - private IMemoryStore? _memoryStorage = null; + private Func? _memoryStorageFactory = null; private IDelegatingHandlerFactory? _httpHandlerFactory = null; private IPromptTemplateEngine? _promptTemplateEngine; private readonly AIServiceCollection _aiServices = new(); @@ -61,9 +61,9 @@ public IKernel Build() ); // TODO: decouple this from 'UseMemory' kernel extension - if (this._memoryStorage != null) + if (this._memoryStorageFactory != null) { - instance.UseMemory(this._memoryStorage); + instance.UseMemory(this._memoryStorageFactory.Invoke()); } return instance; @@ -101,7 +101,19 @@ public KernelBuilder WithMemory(ISemanticTextMemory memory) public KernelBuilder WithMemoryStorage(IMemoryStore storage) { Verify.NotNull(storage); - this._memoryStorage = storage; + this._memoryStorageFactory = () => storage; + return this; + } + + /// + /// Add memory storage factory to the kernel. + /// + /// The storage factory. + /// Updated kernel builder including the memory storage. + public KernelBuilder WithMemoryStorage(Func<(ILogger Logger, KernelConfig Config), TStore> factory) where TStore : IMemoryStore + { + Verify.NotNull(factory); + this._memoryStorageFactory = () => factory((this._logger, this._config)); return this; } diff --git a/samples/dotnet/kernel-syntax-examples/Example38_Pinecone.cs b/samples/dotnet/kernel-syntax-examples/Example38_Pinecone.cs index 9c048fe8d676..7ee2c94ebbf0 100644 --- a/samples/dotnet/kernel-syntax-examples/Example38_Pinecone.cs +++ b/samples/dotnet/kernel-syntax-examples/Example38_Pinecone.cs @@ -32,6 +32,7 @@ public static async Task RunAsync() .WithOpenAITextCompletionService("text-davinci-003", Env.Var("OPENAI_API_KEY")) .WithOpenAITextEmbeddingGenerationService("text-embedding-ada-002", Env.Var("OPENAI_API_KEY")) .WithMemoryStorage(memoryStore) + //.WithPineconeMemoryStore(pineconeEnvironment, apiKey) // This method offers an alternative approach to registering Pinecone memory storage. .Build(); Console.WriteLine("== Printing Collections in DB ==");