Skip to content

Commit

Permalink
.Net: Adding Kusto as an external memory (#2257)
Browse files Browse the repository at this point in the history
### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

Adding Kusto as an external memory.

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄

---------

Co-authored-by: Dmytro Struk <[email protected]>
Co-authored-by: Shawn Callegari <[email protected]>
  • Loading branch information
3 people authored Aug 10, 2023
1 parent bb163b6 commit 4721625
Show file tree
Hide file tree
Showing 14 changed files with 1,194 additions and 0 deletions.
1 change: 1 addition & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageVersion Include="Azure.Identity" Version="1.9.0" />
<PackageVersion Include="Azure.Search.Documents" Version="11.5.0-beta.3" />
<PackageVersion Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />
<PackageVersion Include="Microsoft.Azure.Kusto.Data" Version="11.3.2" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="[1.1.0, )" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
Expand Down
9 changes: 9 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Planning.StepwisePlanner",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationInsightsExample", "samples\ApplicationInsightsExample\ApplicationInsightsExample.csproj", "{C754950A-E16C-4F96-9CC7-9328E361B5AF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Kusto", "src\Connectors\Connectors.Memory.Kusto\Connectors.Memory.Kusto.csproj", "{E07608CC-D710-4655-BB9E-D22CF3CDD193}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -361,6 +363,12 @@ Global
{C754950A-E16C-4F96-9CC7-9328E361B5AF}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{C754950A-E16C-4F96-9CC7-9328E361B5AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C754950A-E16C-4F96-9CC7-9328E361B5AF}.Release|Any CPU.Build.0 = Release|Any CPU
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Publish|Any CPU.Build.0 = Debug|Any CPU
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -413,6 +421,7 @@ Global
{677F1381-7830-4115-9C1A-58B282629DC6} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{4762BCAF-E1C5-4714-B88D-E50FA333C50E} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
{C754950A-E16C-4F96-9CC7-9328E361B5AF} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{E07608CC-D710-4655-BB9E-D22CF3CDD193} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
Expand Down
80 changes: 80 additions & 0 deletions dotnet/samples/KernelSyntaxExamples/Example53_Kusto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Memory.Kusto;
using Microsoft.SemanticKernel.Memory;
using RepoUtils;

// ReSharper disable once InconsistentNaming
public static class Example53_Kusto
{
private const string MemoryCollectionName = "kusto_test";

public static async Task RunAsync()
{
var connectionString = new Kusto.Data.KustoConnectionStringBuilder(TestConfiguration.Kusto.ConnectionString).WithAadUserPromptAuthentication();
using KustoMemoryStore memoryStore = new(connectionString, "MyDatabase");

IKernel kernel = Kernel.Builder
.WithLogger(ConsoleLogger.Logger)
.WithOpenAITextCompletionService(
modelId: TestConfiguration.OpenAI.ModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.WithOpenAITextEmbeddingGenerationService(
modelId: TestConfiguration.OpenAI.EmbeddingModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.WithMemoryStorage(memoryStore)
.Build();

Console.WriteLine("== Printing Collections in DB ==");
var collections = memoryStore.GetCollectionsAsync();
await foreach (var collection in collections)
{
Console.WriteLine(collection);
}

Console.WriteLine("== Adding Memories ==");

var key1 = await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "cat1", text: "british short hair");
var key2 = await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "cat2", text: "orange tabby");
var key3 = await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "cat3", text: "norwegian forest cat");

Console.WriteLine("== Printing Collections in DB ==");
collections = memoryStore.GetCollectionsAsync();
await foreach (var collection in collections)
{
Console.WriteLine(collection);
}

Console.WriteLine("== Retrieving Memories Through the Kernel ==");
MemoryQueryResult? lookup = await kernel.Memory.GetAsync(MemoryCollectionName, "cat1");
Console.WriteLine(lookup != null ? lookup.Metadata.Text : "ERROR: memory not found");

Console.WriteLine("== Retrieving Memories Directly From the Store ==");
var memory1 = await memoryStore.GetAsync(MemoryCollectionName, key1);
var memory2 = await memoryStore.GetAsync(MemoryCollectionName, key2);
var memory3 = await memoryStore.GetAsync(MemoryCollectionName, key3);
Console.WriteLine(memory1 != null ? memory1.Metadata.Text : "ERROR: memory not found");
Console.WriteLine(memory2 != null ? memory2.Metadata.Text : "ERROR: memory not found");
Console.WriteLine(memory3 != null ? memory3.Metadata.Text : "ERROR: memory not found");

Console.WriteLine("== Similarity Searching Memories: My favorite color is orange ==");
var searchResults = kernel.Memory.SearchAsync(MemoryCollectionName, "My favorite color is orange", limit: 3, minRelevanceScore: 0.8);

await foreach (var item in searchResults)
{
Console.WriteLine(item.Metadata.Text + " : " + item.Relevance);
}

Console.WriteLine("== Removing Collection {0} ==", MemoryCollectionName);
await memoryStore.DeleteCollectionAsync(MemoryCollectionName);

Console.WriteLine("== Printing Collections in DB ==");
await foreach (var collection in collections)
{
Console.WriteLine(collection);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<ProjectReference Include="..\..\src\Connectors\Connectors.AI.HuggingFace\Connectors.AI.HuggingFace.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.AzureCognitiveSearch\Connectors.Memory.AzureCognitiveSearch.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Chroma\Connectors.Memory.Chroma.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Kusto\Connectors.Memory.Kusto.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Postgres\Connectors.Memory.Postgres.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Weaviate\Connectors.Memory.Weaviate.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Redis\Connectors.Memory.Redis.csproj" />
Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/KernelSyntaxExamples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public static async Task Main()
await Example50_Chroma.RunAsync().SafeWaitAsync(cancelToken);
await Example51_StepwisePlanner.RunAsync().SafeWaitAsync(cancelToken);
await Example52_ApimAuth.RunAsync().SafeWaitAsync(cancelToken);
await Example53_Kusto.RunAsync().SafeWaitAsync(cancelToken);
}

private static void LoadUserSecrets()
Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/KernelSyntaxExamples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ dotnet user-secrets set "Apim:SubscriptionKey" "..."
dotnet user-secrets set "Postgres:ConnectionString" "..."
dotnet user-secrets set "Redis:Configuration" "..."
dotnet user-secrets set "Kusto:ConnectionString" "..."
```

To set your secrets with environment variables, use these names:
Expand Down
6 changes: 6 additions & 0 deletions dotnet/samples/KernelSyntaxExamples/TestConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static void Initialize(IConfigurationRoot configRoot)
public static RedisConfig Redis => LoadSection<RedisConfig>();
public static JiraConfig Jira => LoadSection<JiraConfig>();
public static ChromaConfig Chroma => LoadSection<ChromaConfig>();
public static KustoConfig Kusto => LoadSection<KustoConfig>();

private static T LoadSection<T>([CallerMemberName] string? caller = null)
{
Expand Down Expand Up @@ -154,5 +155,10 @@ public class ChromaConfig
{
public string Endpoint { get; set; }
}

public class KustoConfig
{
public string ConnectionString { get; set; }
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- THIS PROPERTY GROUP MUST COME FIRST -->
<AssemblyName>Microsoft.SemanticKernel.Connectors.Memory.Kusto</AssemblyName>
<RootNamespace>Microsoft.SemanticKernel.Connectors.Memory.Kusto</RootNamespace>
<TargetFramework>netstandard2.0</TargetFramework>

<!--NU5104: A stable release of a package should not have a prerelease dependency.-->
<NoWarn>NU5104</NoWarn>
</PropertyGroup>

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

<PropertyGroup>
<!-- NuGet Package Settings -->
<PackageId>Microsoft.SemanticKernel.Connectors.Memory.Kusto</PackageId>
<Title>Semantic Kernel - Azure Data Explorer (Kusto) Semantic Memory</Title>
<Description>Azure Data Explorer (Kusto) Semantic Memory connector for Semantic Kernel</Description>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.Kusto.Data" />
</ItemGroup>

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

</Project>
95 changes: 95 additions & 0 deletions dotnet/src/Connectors/Connectors.Memory.Kusto/KustoMemoryRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Kusto.Cloud.Platform.Utils;
using Microsoft.SemanticKernel.AI.Embeddings;
using Microsoft.SemanticKernel.Memory;

namespace Microsoft.SemanticKernel.Connectors.Memory.Kusto;

/// <summary>
/// Kusto memory record entity.
/// </summary>
public sealed class KustoMemoryRecord
{
/// <summary>
/// Entity key.
/// </summary>
public string Key { get; set; }

/// <summary>
/// Metadata associated with memory entity.
/// </summary>
public MemoryRecordMetadata Metadata { get; set; }

/// <summary>
/// Source content embedding.
/// </summary>
public Embedding<float> Embedding { get; set; }

/// <summary>
/// Optional timestamp.
/// </summary>
public DateTimeOffset? Timestamp { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="KustoMemoryRecord"/> class.
/// </summary>
/// <param name="record">Instance of <see cref="MemoryRecord"/>.</param>
public KustoMemoryRecord(MemoryRecord record) : this(record.Key, record.Metadata, record.Embedding, record.Timestamp) { }

/// <summary>
/// Initializes a new instance of the <see cref="KustoMemoryRecord"/> class.
/// </summary>
/// <param name="key">Entity key.</param>
/// <param name="metadata">Metadata associated with memory entity.</param>
/// <param name="embedding">Source content embedding.</param>
/// <param name="timestamp">Optional timestamp.</param>
public KustoMemoryRecord(string key, MemoryRecordMetadata metadata, Embedding<float> embedding, DateTimeOffset? timestamp = null)
{
this.Key = key;
this.Metadata = metadata;
this.Embedding = embedding;
this.Timestamp = timestamp;
}

/// <summary>
/// Initializes a new instance of the <see cref="KustoMemoryRecord"/> class.
/// </summary>
/// <param name="key">Entity key.</param>
/// <param name="metadata">Serialized metadata associated with memory entity.</param>
/// <param name="embedding">Source content embedding.</param>
/// <param name="timestamp">Optional timestamp.</param>
public KustoMemoryRecord(string key, string metadata, string? embedding, string? timestamp = null)
{
this.Key = key;
this.Metadata = KustoSerializer.DeserializeMetadata(metadata);
this.Embedding = KustoSerializer.DeserializeEmbedding(embedding);
this.Timestamp = KustoSerializer.DeserializeDateTimeOffset(timestamp);
}

/// <summary>
/// Returns instance of mapped <see cref="MemoryRecord"/>.
/// </summary>
public MemoryRecord ToMemoryRecord()
{
return new MemoryRecord(this.Metadata, this.Embedding, this.Key, this.Timestamp);
}

/// <summary>
/// Writes properties of <see cref="KustoMemoryRecord"/> instance to stream using <see cref="CsvWriter"/>.
/// </summary>
/// <param name="streamWriter">Instance of <see cref="CsvWriter"/> to write properties to stream.</param>
public void WriteToCsvStream(CsvWriter streamWriter)
{
var jsonifiedMetadata = KustoSerializer.SerializeMetadata(this.Metadata);
var jsonifiedEmbedding = KustoSerializer.SerializeEmbedding(this.Embedding);
var isoFormattedDate = KustoSerializer.SerializeDateTimeOffset(this.Timestamp);

streamWriter.WriteField(this.Key);
streamWriter.WriteField(jsonifiedMetadata);
streamWriter.WriteField(jsonifiedEmbedding);
streamWriter.WriteField(isoFormattedDate);
streamWriter.CompleteRecord();
}
}
Loading

0 comments on commit 4721625

Please sign in to comment.