Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http consistency: Azure Cognitive Search Memory #1426

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
namespace Microsoft.SemanticKernel;
#pragma warning restore IDE0130

/// <summary>
/// Provides extension methods for the <see cref="KernelBuilder"/> class to configure OpenAI and AzureOpenAI connectors.
/// </summary>
public static class OpenAIKernelBuilderExtensions
{
#region Text Completion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Azure;
using Azure.Core;
using Azure.Core.Pipeline;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
Expand All @@ -32,21 +34,37 @@ public class AzureCognitiveSearchMemory : ISemanticTextMemory
/// Create a new instance of semantic memory using Azure Cognitive Search.
/// </summary>
/// <param name="endpoint">Azure Cognitive Search URI, e.g. "https://contoso.search.windows.net"</param>
/// <param name="apiKey">API Key</param>
public AzureCognitiveSearchMemory(string endpoint, string apiKey)
/// <param name="apiKey">The Api key used to authenticate requests against the Search service.</param>
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
public AzureCognitiveSearchMemory(string endpoint, string apiKey, HttpClient? httpClient = null)
{
var options = new SearchClientOptions();

if (httpClient != null)
{
options.Transport = new HttpClientTransport(httpClient);
}

AzureKeyCredential credentials = new(apiKey);
this._adminClient = new SearchIndexClient(new Uri(endpoint), credentials);
this._adminClient = new SearchIndexClient(new Uri(endpoint), credentials, options);
}

/// <summary>
/// Create a new instance of semantic memory using Azure Cognitive Search.
/// </summary>
/// <param name="endpoint">Azure Cognitive Search URI, e.g. "https://contoso.search.windows.net"</param>
/// <param name="credentials">Azure service</param>
public AzureCognitiveSearchMemory(string endpoint, TokenCredential credentials)
/// <param name="credentials">The token credential used to authenticate requests against the Search service.</param>
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
public AzureCognitiveSearchMemory(string endpoint, TokenCredential credentials, HttpClient? httpClient = null)
{
this._adminClient = new SearchIndexClient(new Uri(endpoint), credentials);
var options = new SearchClientOptions();

if (httpClient != null)
{
options.Transport = new HttpClientTransport(httpClient);
}

this._adminClient = new SearchIndexClient(new Uri(endpoint), credentials, options);
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Net.Http;
using Azure.Core;
using Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch;

#pragma warning disable IDE0130
namespace Microsoft.SemanticKernel;
#pragma warning restore IDE0130

/// <summary>
/// Provides extension methods for the <see cref="KernelBuilder"/> class to configure Azure Cognitive Search connectors.
/// </summary>
public static class AzureSearchServiceKernelBuilderExtensions
{
/// <summary>
/// Registers Azure Cognitive Search Memory Store.
/// </summary>
/// <param name="builder">The <see cref="KernelBuilder"/> instance</param>
/// <param name="endpoint">Azure Cognitive Search URI, e.g. "https://contoso.search.windows.net"</param>
/// <param name="apiKey">The Api key used to authenticate requests against the Search service.</param>
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
/// <returns>Self instance</returns>
public static KernelBuilder WithAzureAzureCognitiveSearchMemory(this KernelBuilder builder,
SergeyMenshykh marked this conversation as resolved.
Show resolved Hide resolved
string endpoint,
string apiKey,
HttpClient? httpClient = null)
{
builder.WithMemory((parameters) =>
{
return new AzureCognitiveSearchMemory(
endpoint,
apiKey,
HttpClientProvider.GetHttpClient(parameters.Config, httpClient, parameters.Logger));
});

return builder;
}

/// <summary>
/// Registers Azure Cognitive Search Memory Store.
/// </summary>
/// <param name="builder">The <see cref="KernelBuilder"/> instance</param>
/// <param name="endpoint">Azure Cognitive Search URI, e.g. "https://contoso.search.windows.net"</param>
/// <param name="credentials">The token credential used to authenticate requests against the Search service.</param>
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
/// <returns>Self instance</returns>
public static KernelBuilder WithAzureCognitiveSearchMemory(this KernelBuilder builder,
string endpoint,
TokenCredential credentials,
HttpClient? httpClient = null)
{
builder.WithMemory((parameters) =>
{
return new AzureCognitiveSearchMemory(
endpoint,
credentials,
HttpClientProvider.GetHttpClient(parameters.Config, httpClient, parameters.Logger));
});

return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<!-- IMPORT NUGET PACKAGE SHARED PROPERTIES -->
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<Import Project="$(RepoRoot)/dotnet/src/InternalUtilities/InternalUtilities.props" />

<PropertyGroup>
<!-- NuGet Package Settings -->
Expand All @@ -24,7 +25,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\SemanticKernel\SemanticKernel.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<ProjectReference Include="..\..\SemanticKernel\SemanticKernel.csproj" />
<ProjectReference Include="..\Connectors.AI.HuggingFace\Connectors.AI.HuggingFace.csproj" />
<ProjectReference Include="..\Connectors.AI.OpenAI\Connectors.AI.OpenAI.csproj" />
<ProjectReference Include="..\Connectors.Memory.AzureCognitiveSearch\Connectors.Memory.AzureCognitiveSearch.csproj" />
<ProjectReference Include="..\Connectors.Memory.Pinecone\Connectors.Memory.Pinecone.csproj" />
<ProjectReference Include="..\Connectors.Memory.DuckDB\Connectors.Memory.DuckDB.csproj" />
<ProjectReference Include="..\Connectors.Memory.Qdrant\Connectors.Memory.Qdrant.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch;
using Xunit;

namespace SemanticKernel.Connectors.UnitTests.Memory.AzureCognitiveSearch;

/// <summary>
/// Unit tests for <see cref="AzureCognitiveSearchMemoryTests"/> class.
SergeyMenshykh marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public sealed class AzureCognitiveSearchMemoryTests : IDisposable
{
private HttpMessageHandlerStub messageHandlerStub;
private HttpClient httpClient;

public AzureCognitiveSearchMemoryTests()
{
this.messageHandlerStub = new HttpMessageHandlerStub();

this.httpClient = new HttpClient(this.messageHandlerStub, false);
}

[Fact]
public async Task CustomHttpClientProvidedToFirstConstructorShouldBeUsed()
{
//Arrange
var sut = new AzureCognitiveSearchMemory("https://fake-random-test-host/fake-path", "fake-api-key", this.httpClient);

//Act
await sut.GetAsync("fake-collection", "fake-query");

//Assert
Assert.StartsWith("https://fake-random-test-host/fake-path/indexes('fake-collection')", this.messageHandlerStub.RequestUri?.AbsoluteUri, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public async Task CustomHttpClientProvidedToSecondConstructorShouldBeUsed()
{
//Arrange
var credentials = DelegatedTokenCredential.Create((_, __) => new AccessToken("fake-access-token", DateTimeOffset.UtcNow.AddMinutes(15)));

var sut = new AzureCognitiveSearchMemory("https://fake-random-test-host/fake-path", credentials, this.httpClient);

//Act
await sut.GetAsync("fake-collection", "fake-key");

//Assert
Assert.StartsWith("https://fake-random-test-host/fake-path/indexes('fake-collection')", this.messageHandlerStub.RequestUri?.AbsoluteUri, StringComparison.OrdinalIgnoreCase);
}

public void Dispose()
{
this.httpClient.Dispose();
this.messageHandlerStub.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -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.AzureCognitiveSearch;

public sealed class AzureSearchServiceKernelBuilderExtensionsTests : IDisposable
{
private HttpMessageHandlerStub messageHandlerStub;
private HttpClient httpClient;

public AzureSearchServiceKernelBuilderExtensionsTests()
{
this.messageHandlerStub = new HttpMessageHandlerStub();

this.httpClient = new HttpClient(this.messageHandlerStub, false);
}

[Fact]
public async Task AzureCognitiveSearchMemoryStoreShouldBeProperlyInitialized()
{
//Arrange
this.messageHandlerStub.ResponseToReturn.Content = new StringContent("{\"value\": [{\"name\": \"fake-index1\"}]}", Encoding.UTF8, MediaTypeNames.Application.Json);

var builder = new KernelBuilder();
builder.WithAzureAzureCognitiveSearchMemory("https://fake-random-test-host/fake-path", "fake-api-key", this.httpClient);
builder.WithAzureTextEmbeddingGenerationService("fake-deployment-name", "https://fake-random-test-host/fake-path1", "fake -api-key");
var kernel = builder.Build(); //This call triggers the internal factory registered by WithAzureAzureCognitiveSearchMemory method to create an instance of the AzureCognitiveSearchMemory class.

//Act
await kernel.Memory.GetCollectionsAsync(); //This call triggers a subsequent call to Azure Cognitive Search Memory store.

//Assert
Assert.Equal("https://fake-random-test-host/fake-path/indexes?$select=%2A&api-version=2021-04-30-Preview", this.messageHandlerStub?.RequestUri?.AbsoluteUri);

var headerValues = Enumerable.Empty<string>();
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();
}
}
20 changes: 16 additions & 4 deletions dotnet/src/SemanticKernel/KernelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Microsoft.SemanticKernel;
public sealed class KernelBuilder
{
private KernelConfig _config = new();
private ISemanticTextMemory _memory = NullMemory.Instance;
private Func<ISemanticTextMemory> _memoryFactory = () => NullMemory.Instance;
private ILogger _logger = NullLogger.Instance;
private Func<IMemoryStore>? _memoryStorageFactory = null;
private IDelegatingHandlerFactory? _httpHandlerFactory = null;
Expand Down Expand Up @@ -54,7 +54,7 @@ public IKernel Build()
new SkillCollection(this._logger),
this._aiServices.Build(),
this._promptTemplateEngine ?? new PromptTemplateEngine(this._logger),
this._memory,
this._memoryFactory.Invoke(),
this._config,
this._logger,
this._trustService
Expand Down Expand Up @@ -89,7 +89,19 @@ public KernelBuilder WithLogger(ILogger log)
public KernelBuilder WithMemory(ISemanticTextMemory memory)
{
Verify.NotNull(memory);
this._memory = memory;
this._memoryFactory = () => memory;
return this;
}

/// <summary>
/// Add a semantic text memory store factory.
/// </summary>
/// <param name="factory">The store factory.</param>
/// <returns>Updated kernel builder including the semantic text memory entity.</returns>
public KernelBuilder WithMemory<TStore>(Func<(ILogger Logger, KernelConfig Config), TStore> factory) where TStore : ISemanticTextMemory
SergeyMenshykh marked this conversation as resolved.
Show resolved Hide resolved
{
Verify.NotNull(factory);
this._memoryFactory = () => factory((this._logger, this._config));
return this;
}

Expand Down Expand Up @@ -140,7 +152,7 @@ public KernelBuilder WithPromptTemplateEngine(IPromptTemplateEngine promptTempla
{
Verify.NotNull(storage);
Verify.NotNull(embeddingGenerator);
this._memory = new SemanticTextMemory(storage, embeddingGenerator);
this._memoryFactory = () => new SemanticTextMemory(storage, embeddingGenerator);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch;
using Microsoft.SemanticKernel.Memory;
using RepoUtils;

Expand Down Expand Up @@ -36,7 +35,7 @@ public static async Task RunAsync()

var kernelWithACS = Kernel.Builder
.WithLogger(ConsoleLogger.Log)
.WithMemory(new AzureCognitiveSearchMemory(Env.Var("ACS_ENDPOINT"), Env.Var("ACS_API_KEY")))
.WithAzureAzureCognitiveSearchMemory(Env.Var("ACS_ENDPOINT"), Env.Var("ACS_API_KEY"))
.Build();

await RunExampleAsync(kernelWithACS);
Expand Down
Loading