diff --git a/Directory.Packages.props b/Directory.Packages.props index dea329a59..3f4ce271e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,6 +16,7 @@ + diff --git a/KernelMemory.sln b/KernelMemory.sln index 7ceee262b..2ff409a3a 100644 --- a/KernelMemory.sln +++ b/KernelMemory.sln @@ -226,6 +226,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testapps", "testapps", "{AE EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "213-onnx", "examples\213-onnx\213-onnx.csproj", "{E7ECB0D7-A4AA-4529-B191-3FDFE8674784}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExtensionsAI", "extensions\MicrosoftExtensionsAI\ExtensionsAI.csproj", "{B9E80FE9-ACB8-0C00-FBFE-3D0528BE9B8D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -535,6 +537,10 @@ Global {E7ECB0D7-A4AA-4529-B191-3FDFE8674784}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7ECB0D7-A4AA-4529-B191-3FDFE8674784}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7ECB0D7-A4AA-4529-B191-3FDFE8674784}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9E80FE9-ACB8-0C00-FBFE-3D0528BE9B8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9E80FE9-ACB8-0C00-FBFE-3D0528BE9B8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9E80FE9-ACB8-0C00-FBFE-3D0528BE9B8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9E80FE9-ACB8-0C00-FBFE-3D0528BE9B8D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -576,18 +582,22 @@ Global {40CB6452-68DD-4C78-9852-78281A161950} = {155DA079-E267-49AF-973A-D1D44681970F} {CFE7C192-2561-40CC-8592-136293451EC1} = {155DA079-E267-49AF-973A-D1D44681970F} {93FA6DD6-D0B2-4751-8680-3F959E1F7AF2} = {155DA079-E267-49AF-973A-D1D44681970F} + {11445C36-1B94-4AFB-AC23-976C94924603} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} {8F2185AB-F87C-4DD0-9DB8-E97920500A37} = {155DA079-E267-49AF-973A-D1D44681970F} {E2AD3323-42AA-47D5-87E4-1E90B0A7C6E9} = {3C17F42B-CFC8-4900-8CFB-88936311E919} {9F564F2D-EADD-47DE-9293-92B3E9CFFE36} = {3C17F42B-CFC8-4900-8CFB-88936311E919} {6577B501-E295-4DCE-B640-BF40C9881A00} = {155DA079-E267-49AF-973A-D1D44681970F} {0675D9B5-B3BB-4C9A-A1A5-11540E2ED715} = {3C17F42B-CFC8-4900-8CFB-88936311E919} + {E9EAD4A8-05B8-4138-8B17-E33127C83CF4} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} {14106EE4-AB77-494D-B16A-3EC042539456} = {3C17F42B-CFC8-4900-8CFB-88936311E919} {969625B8-039F-4E5E-A484-6A30DC417FBB} = {155DA079-E267-49AF-973A-D1D44681970F} {9D3C9277-648D-441D-834D-565076EE2E87} = {3C17F42B-CFC8-4900-8CFB-88936311E919} + {CCA96699-483E-4B2A-95DF-25F0C98E3BB6} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} {F25BC232-6161-4D45-8AFF-E92A52F0F85A} = {155DA079-E267-49AF-973A-D1D44681970F} {C8A18E8F-34CC-4226-9831-083335DAB21A} = {3C17F42B-CFC8-4900-8CFB-88936311E919} {9D078C9B-42A3-4558-898A-2396F59A54D3} = {155DA079-E267-49AF-973A-D1D44681970F} {9FB79D60-E87E-420A-984D-C4D5DD2C3125} = {3C17F42B-CFC8-4900-8CFB-88936311E919} + {3897E2B7-85A3-4D69-B1C0-AEF41236F940} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} {62B96766-AA6C-4CFF-A6FB-6370C89C2509} = {3C17F42B-CFC8-4900-8CFB-88936311E919} {E3877E49-958E-4DC8-B5E8-834010F5C4B7} = {155DA079-E267-49AF-973A-D1D44681970F} {A6AE31A1-4F60-47B0-8534-7B083D68118C} = {155DA079-E267-49AF-973A-D1D44681970F} @@ -619,6 +629,7 @@ Global {F192513B-265B-4943-A2A9-44E23B15BA18} = {155DA079-E267-49AF-973A-D1D44681970F} {7BBD348E-CDD9-4462-B8C9-47613C5EC682} = {3C17F42B-CFC8-4900-8CFB-88936311E919} {345DEF9B-6EE1-49DF-B46A-25E38CE9B151} = {155DA079-E267-49AF-973A-D1D44681970F} + {82670921-FDCD-4672-84BD-4353F5AC24A0} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} {58E65B3F-EFF0-401A-AC76-A49835AE0220} = {155DA079-E267-49AF-973A-D1D44681970F} {AB097B62-5A0B-4D74-9F8B-A41FE8241447} = {155DA079-E267-49AF-973A-D1D44681970F} {8E907766-4A7D-46E2-B5E3-EB2994B1AA54} = {3C17F42B-CFC8-4900-8CFB-88936311E919} @@ -629,13 +640,9 @@ Global {BFF9BE1A-B0E4-4ABE-B384-01B200D4FEFB} = {155DA079-E267-49AF-973A-D1D44681970F} {FD1EB2C1-581E-4EB8-AF4A-BC4773453226} = {3C17F42B-CFC8-4900-8CFB-88936311E919} {D6BC74A5-41C7-4A60-9C2E-F246DC40145A} = {87DEAE8D-138C-4FDD-B4C9-11C3A7817E8F} - {11445C36-1B94-4AFB-AC23-976C94924603} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} - {E9EAD4A8-05B8-4138-8B17-E33127C83CF4} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} - {3897E2B7-85A3-4D69-B1C0-AEF41236F940} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} - {82670921-FDCD-4672-84BD-4353F5AC24A0} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} - {CCA96699-483E-4B2A-95DF-25F0C98E3BB6} = {AEF463F6-F813-498C-830A-3B4CED6DC4A7} {AEF463F6-F813-498C-830A-3B4CED6DC4A7} = {5E7DD43D-B5E7-4827-B57D-447E5B428589} {E7ECB0D7-A4AA-4529-B191-3FDFE8674784} = {0A43C65C-6007-4BB4-B3FE-8D439FC91841} + {B9E80FE9-ACB8-0C00-FBFE-3D0528BE9B8D} = {155DA079-E267-49AF-973A-D1D44681970F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CC136C62-115C-41D1-B414-F9473EFF6EA8} diff --git a/extensions/MicrosoftExtensionsAI/DependencyInjection.cs b/extensions/MicrosoftExtensionsAI/DependencyInjection.cs new file mode 100644 index 000000000..9e89a915b --- /dev/null +++ b/extensions/MicrosoftExtensionsAI/DependencyInjection.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.KernelMemory.AI; +using Microsoft.KernelMemory.AI.ExtensionsAI; + +#pragma warning disable IDE0130 // reduce number of "using" statements +// ReSharper disable once CheckNamespace - reduce number of "using" statements +namespace Microsoft.KernelMemory; + +/// +/// Kernel Memory builder extensions +/// +public static partial class KernelMemoryBuilderExtensions +{ + /// + /// Use an as an to generate text completions with this . + /// + /// The builder + /// The to use for text generation. + /// Optional configuration for the instance. + /// Optional text tokenizer to use for token counting. + /// The builder provided as . + public static IKernelMemoryBuilder WithChatClient( + this IKernelMemoryBuilder builder, + IChatClient chatClient, + ExtensionsAIConfig? config = null, + ITextTokenizer? tokenizer = null) + { + ArgumentNullExceptionEx.ThrowIfNull(builder); + ArgumentNullExceptionEx.ThrowIfNull(chatClient); + + builder.Services.AddSingleton(serviceProvider => + new ExtensionsAITextGenerator(chatClient, config, tokenizer, serviceProvider.GetService())); + + return builder; + } + + /// + /// Use to generate text embeddings. + /// + /// Kernel Memory builder + /// The to use for embedding generation. + /// Optional configuration for the instance. + /// Optional text tokenizer to use for token counting. + /// Whether to use the only for retrieval, not for ingestion. + /// The builder provided as . + public static IKernelMemoryBuilder WithEmbeddingGenerator( + this IKernelMemoryBuilder builder, + IEmbeddingGenerator> embeddingGenerator, + ExtensionsAIConfig? config = null, + ITextTokenizer? tokenizer = null, + bool onlyForRetrieval = false) + { + ArgumentNullExceptionEx.ThrowIfNull(builder); + ArgumentNullExceptionEx.ThrowIfNull(embeddingGenerator); + + builder.Services.AddSingleton(serviceProvider => new ExtensionsAIEmbeddingGenerator( + embeddingGenerator, config, tokenizer, serviceProvider.GetService())); + + if (!onlyForRetrieval) + { + builder.AddIngestionEmbeddingGenerator(new ExtensionsAIEmbeddingGenerator(embeddingGenerator, config, tokenizer)); + } + + return builder; + } +} diff --git a/extensions/MicrosoftExtensionsAI/ExtensionsAI.csproj b/extensions/MicrosoftExtensionsAI/ExtensionsAI.csproj new file mode 100644 index 000000000..3f9b7cc83 --- /dev/null +++ b/extensions/MicrosoftExtensionsAI/ExtensionsAI.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + LatestMajor + Microsoft.KernelMemory.AI.ExtensionsAI + Microsoft.KernelMemory.AI.ExtensionsAI + $(NoWarn);KMEXP00;KMEXP01;CA1724;CA1308;SKEXP0010;SKEXP0011;SKEXP0001;CA2208; + + + + + + + + + + + + + true + Microsoft.KernelMemory.AI.ExtensionsAI + Microsoft.Extensions.AI connector for Kernel Memory + Enables using any Microsoft.Extensions.AI IChatClient or IEmbeddingGenerator with Kernel Memory. + Microsoft.Extensions.AI, IChatClient, OpenAI, Plugin, Memory, RAG, Kernel Memory, Azure Blob, Semantic Memory, Episodic Memory, Declarative Memory, AI, Artificial Intelligence, Embeddings, Vector DB, Vector Search, Memory DB, ETL + bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml + + + + + + + diff --git a/extensions/MicrosoftExtensionsAI/ExtensionsAIConfig.cs b/extensions/MicrosoftExtensionsAI/ExtensionsAIConfig.cs new file mode 100644 index 000000000..e8ed7630e --- /dev/null +++ b/extensions/MicrosoftExtensionsAI/ExtensionsAIConfig.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0130 // reduce number of "using" statements +// ReSharper disable once CheckNamespace - reduce number of "using" statements + +namespace Microsoft.KernelMemory; + +public class ExtensionsAIConfig +{ + /// Gets or sets the maximum length of the response that the model will generate. + public int MaxTokens { get; set; } = 8192; + + /// Gets or sets the name of the tokenizer used to count tokens. + public string Tokenizer { get; set; } = "o200k"; +} diff --git a/extensions/MicrosoftExtensionsAI/ExtensionsAIEmbeddingGenerator.cs b/extensions/MicrosoftExtensionsAI/ExtensionsAIEmbeddingGenerator.cs new file mode 100644 index 000000000..1c94c4526 --- /dev/null +++ b/extensions/MicrosoftExtensionsAI/ExtensionsAIEmbeddingGenerator.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.KernelMemory.Diagnostics; + +namespace Microsoft.KernelMemory.AI.ExtensionsAI; + +/// Provides an that wraps an . +public sealed class ExtensionsAIEmbeddingGenerator : ITextEmbeddingGenerator, ITextEmbeddingBatchGenerator +{ + private readonly IEmbeddingGenerator> _embeddingGenerator; + private readonly ITextTokenizer _textTokenizer; + private readonly ILogger _log; + + /// + public int MaxTokens { get; } + + /// + public int MaxBatchSize { get; } + + /// Initializes a new instance of the class. + /// The underlying . + /// Optional configuration for the instance. + /// Optional text tokenizer to use for token counting. + /// Optional logging factory to use for logging. + public ExtensionsAIEmbeddingGenerator( + IEmbeddingGenerator> embeddingGenerator, + ExtensionsAIConfig? config = null, + ITextTokenizer? textTokenizer = null, + ILoggerFactory? loggerFactory = null) + { + ArgumentNullExceptionEx.ThrowIfNull(embeddingGenerator); + + config ??= new(); + + this._embeddingGenerator = embeddingGenerator; + this._log = (loggerFactory ?? DefaultLogger.Factory).CreateLogger(); + this.MaxTokens = config.MaxTokens; + this.MaxBatchSize = 1; + this._textTokenizer = textTokenizer ?? TokenizerFactory.GetTokenizerForEncoding(string.IsNullOrEmpty(config.Tokenizer) ? "o200k" : config.Tokenizer)!; + } + + /// + public int CountTokens(string text) => this._textTokenizer.CountTokens(text); + + /// + public IReadOnlyList GetTokens(string text) => this._textTokenizer.GetTokens(text); + + /// + public async Task GenerateEmbeddingAsync(string text, CancellationToken cancellationToken = default) + { + ArgumentNullExceptionEx.ThrowIfNull(text); + var results = await this.GenerateEmbeddingBatchAsync([text], cancellationToken).ConfigureAwait(false); + if (results.Length != 1) + { + throw new InvalidOperationException($"Expected exactly one embedding result, but received {results.Length}."); + } + + return results[0]; + } + + /// + public async Task GenerateEmbeddingBatchAsync(IEnumerable textList, CancellationToken cancellationToken = default) + { + ArgumentNullExceptionEx.ThrowIfNull(textList); + + var results = await this._embeddingGenerator.GenerateAsync(textList, cancellationToken: cancellationToken).ConfigureAwait(false); + return results.Select(embedding => new Embedding(embedding.Vector)).ToArray(); + } +} diff --git a/extensions/MicrosoftExtensionsAI/ExtensionsAITextGenerator.cs b/extensions/MicrosoftExtensionsAI/ExtensionsAITextGenerator.cs new file mode 100644 index 000000000..1105a0acd --- /dev/null +++ b/extensions/MicrosoftExtensionsAI/ExtensionsAITextGenerator.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.KernelMemory.Diagnostics; + +namespace Microsoft.KernelMemory.AI.ExtensionsAI; + +/// Provides an that wraps an . +public sealed class ExtensionsAITextGenerator : ITextGenerator +{ + private readonly IChatClient _client; + private readonly ITextTokenizer _textTokenizer; + private readonly ILogger _log; + private readonly string _textModel; + + /// Initializes a new instance of the class. + /// The underlying . + /// Optional configuration for the instance. + /// Optional text tokenizer to use for token counting. + /// Optional logging factory to use for logging. + public ExtensionsAITextGenerator( + IChatClient chatClient, + ExtensionsAIConfig? config = null, + ITextTokenizer? textTokenizer = null, + ILoggerFactory? loggerFactory = null) + { + ArgumentNullExceptionEx.ThrowIfNull(chatClient); + + config ??= new(); + + this._client = chatClient; + this._log = (loggerFactory ?? DefaultLogger.Factory).CreateLogger(); + this._textModel = this._client.GetService()?.DefaultModelId ?? ""; + this.MaxTokenTotal = config.MaxTokens; + this._textTokenizer = textTokenizer ?? TokenizerFactory.GetTokenizerForEncoding(string.IsNullOrEmpty(config.Tokenizer) ? "o200k" : config.Tokenizer)!; + } + + /// + public int MaxTokenTotal { get; } + + /// + public int CountTokens(string text) => this._textTokenizer.CountTokens(text); + + /// + public IReadOnlyList GetTokens(string text) => this._textTokenizer.GetTokens(text); + + /// + public async IAsyncEnumerable GenerateTextAsync( + string prompt, + TextGenerationOptions options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ChatOptions chatOptions = new() + { + FrequencyPenalty = (float)options.FrequencyPenalty, + MaxOutputTokens = options.MaxTokens ?? this.MaxTokenTotal, + PresencePenalty = (float)options.PresencePenalty, + StopSequences = options.StopSequences, + Temperature = (float)options.Temperature, + TopP = (float)options.NucleusSampling, + }; + + string? responseModel = null; + await foreach (var update in this._client.GetStreamingResponseAsync(prompt, chatOptions, cancellationToken).WithCancellation(cancellationToken)) + { + responseModel ??= update.ModelId; + foreach (var content in update.Contents) + { + switch (content) + { + case Microsoft.Extensions.AI.TextContent tc: + yield return new GeneratedTextContent(tc.Text, null); + break; + + case UsageContent uc: + yield return new GeneratedTextContent(string.Empty, new TokenUsage + { + Timestamp = update.CreatedAt ?? DateTimeOffset.UtcNow, + ModelType = Constants.ModelType.TextGeneration, + ModelName = responseModel ?? this._textModel, + ServiceTokensIn = (int?)uc.Details.InputTokenCount, + ServiceTokensOut = (int?)uc.Details.OutputTokenCount, + }); + break; + } + } + } + } +} diff --git a/extensions/MicrosoftExtensionsAI/README.md b/extensions/MicrosoftExtensionsAI/README.md new file mode 100644 index 000000000..2cba62689 --- /dev/null +++ b/extensions/MicrosoftExtensionsAI/README.md @@ -0,0 +1,6 @@ +# Kernel Memory with Microsoft.Extensions.AI + +[![Nuget package](https://img.shields.io/nuget/v/Microsoft.KernelMemory.AI.ExtensionsAI)](https://www.nuget.org/packages/Microsoft.KernelMemory.AI.ExtensionsAI/) +[![Discord](https://img.shields.io/discord/1063152441819942922?label=Discord&logo=discord&logoColor=white&color=d82679)](https://aka.ms/KMdiscord) + +This project contains the [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/latest) connector for using any IChatClient or IEmbeddingGenerator with Kernel Memory.