diff --git a/KernelMemory.sln b/KernelMemory.sln index d11ab1451..59c2ae5bf 100644 --- a/KernelMemory.sln +++ b/KernelMemory.sln @@ -230,6 +230,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MongoDbAtlas", "extensions\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MongoDbAtlas.FunctionalTests", "extensions\MongoDbAtlas\MongoDbAtlas.FunctionalTests\MongoDbAtlas.FunctionalTests.csproj", "{8A602227-B291-4F1B-ACB8-237F49501B6A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "107-dotnet-SemanticKernel-TextCompletion", "examples\107-dotnet-SemanticKernel-TextCompletion\107-dotnet-SemanticKernel-TextCompletion.csproj", "{494B8590-F0B2-4D40-A895-F9D7BDF26250}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -298,6 +300,7 @@ Global {A5CE3CB2-F746-4832-B239-1A3226F9BD48} = {0A43C65C-6007-4BB4-B3FE-8D439FC91841} {4A539320-EB49-4688-82E1-4FA0ADBB3810} = {155DA079-E267-49AF-973A-D1D44681970F} {8A602227-B291-4F1B-ACB8-237F49501B6A} = {3C17F42B-CFC8-4900-8CFB-88936311E919} + {494B8590-F0B2-4D40-A895-F9D7BDF26250} = {0A43C65C-6007-4BB4-B3FE-8D439FC91841} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8A9FA587-7EBA-4D43-BE47-38D798B1C74C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -486,5 +489,8 @@ Global {8A602227-B291-4F1B-ACB8-237F49501B6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8A602227-B291-4F1B-ACB8-237F49501B6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A602227-B291-4F1B-ACB8-237F49501B6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {494B8590-F0B2-4D40-A895-F9D7BDF26250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {494B8590-F0B2-4D40-A895-F9D7BDF26250}.Debug|Any CPU.Build.0 = Debug|Any CPU + {494B8590-F0B2-4D40-A895-F9D7BDF26250}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection EndGlobal diff --git a/examples/107-dotnet-SemanticKernel-TextCompletion/107-dotnet-SemanticKernel-TextCompletion.csproj b/examples/107-dotnet-SemanticKernel-TextCompletion/107-dotnet-SemanticKernel-TextCompletion.csproj new file mode 100644 index 000000000..cfa574100 --- /dev/null +++ b/examples/107-dotnet-SemanticKernel-TextCompletion/107-dotnet-SemanticKernel-TextCompletion.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + _107_dotnet_SemanticKernel_TextCompletion + enable + enable + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + $(NoWarn);SKEXP0001;SKEXP0010; + + + + + + + diff --git a/examples/107-dotnet-SemanticKernel-TextCompletion/Program.cs b/examples/107-dotnet-SemanticKernel-TextCompletion/Program.cs new file mode 100644 index 000000000..a05c5b7e3 --- /dev/null +++ b/examples/107-dotnet-SemanticKernel-TextCompletion/Program.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.KernelMemory; +using Microsoft.KernelMemory.AI.OpenAI; +using Microsoft.KernelMemory.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +var endpoint = Env.Var("AOAI_ENDPOINT"); +var apiKey = Env.Var("AOAI_API_KEY"); +var chatDeployment = Env.Var("AOAI_DEPLOYMENT_CHAT"); +var embeddingDeployment = Env.Var("AOAI_DEPLOYMENT_EMBEDDING"); + +var config = new SemanticKernelConfig(); +var tokenizer = new DefaultGPTTokenizer(); + +var memory = new KernelMemoryBuilder() + .WithSemanticKernelTextGenerationService( + new AzureOpenAIChatCompletionService(chatDeployment, endpoint, apiKey), config, tokenizer) + .WithSemanticKernelTextEmbeddingGenerationService( + new AzureOpenAITextEmbeddingGenerationService(embeddingDeployment, endpoint, apiKey), config, tokenizer) + .Build(); + +await memory.ImportWebPageAsync("https://raw.githubusercontent.com/microsoft/kernel-memory/main/COMMUNITY.md", documentId: "doc001"); + +var question = "How can I join Kernel Memory's Discord?"; +Console.WriteLine($"\n\nQuestion: {question}"); + +var answer = await memory.AskAsync(question); +Console.WriteLine($"\nAnswer: {answer.Result}"); + +Console.WriteLine("\n\n Sources:\n"); +foreach (var x in answer.RelevantSources) +{ + Console.WriteLine($" - {x.SourceName} - {x.Link} [{x.Partitions.First().LastUpdate:D}]"); +} diff --git a/service/Core/Core.csproj b/service/Core/Core.csproj index cee218b80..8b9259efd 100644 --- a/service/Core/Core.csproj +++ b/service/Core/Core.csproj @@ -5,7 +5,7 @@ LatestMajor Microsoft.KernelMemory.Core Microsoft.KernelMemory - $(NoWarn);SKEXP0011;CA2208;CA1308;CA1724;CS1591; + $(NoWarn);SKEXP0001;SKEXP0011;CA2208;CA1308;CA1724;CS1591; $(DefineConstants);KernelMemoryDev diff --git a/service/Core/SemanticKernel/KernelMemoryBuilderExtensions.cs b/service/Core/SemanticKernel/KernelMemoryBuilderExtensions.cs new file mode 100644 index 000000000..0695b2df9 --- /dev/null +++ b/service/Core/SemanticKernel/KernelMemoryBuilderExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Logging; +using Microsoft.KernelMemory.AI; +using Microsoft.KernelMemory.Configuration; +using Microsoft.KernelMemory.SemanticKernel; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.TextGeneration; + +#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 +{ + /// + /// Inject an implementation of SK text generation service + /// for local dependencies on + /// + /// KM builder + /// SK text generation service instance + /// SK text generator settings + /// Tokenizer used to count tokens used by prompts + /// .NET logger factory + /// KM builder + public static IKernelMemoryBuilder WithSemanticKernelTextGenerationService( + this IKernelMemoryBuilder builder, + ITextGenerationService service, + SemanticKernelConfig config, + ITextTokenizer? tokenizer = null, + ILoggerFactory? loggerFactory = null) + { + if (service == null) { throw new ConfigurationException("The semantic kernel text generation service instance is NULL"); } + + return builder.AddSingleton(new SemanticKernelTextGenerator(service, config, tokenizer, loggerFactory)); + } + + /// + /// Inject an implementation ofSK text embedding generation service + /// for local dependencies on + /// + /// KM builder + /// SK text embedding generation instance + /// SK text embedding generator settings + /// Tokenizer used to count tokens sent to the embedding generator + /// .NET logger factory + /// Whether to use this embedding generator only during data ingestion, and not for retrieval (search and ask API) + /// KM builder + public static IKernelMemoryBuilder WithSemanticKernelTextEmbeddingGenerationService( + this IKernelMemoryBuilder builder, + ITextEmbeddingGenerationService service, + SemanticKernelConfig config, + ITextTokenizer? tokenizer = null, + ILoggerFactory? loggerFactory = null, + bool onlyForRetrieval = false) + { + if (service == null) { throw new ConfigurationException("The semantic kernel text embedding generation service instance is NULL"); } + + var generator = new SemanticKernelTextEmbeddingGenerator(service, config, tokenizer, loggerFactory); + builder.AddSingleton(generator); + + if (!onlyForRetrieval) + { + builder.AddIngestionEmbeddingGenerator(generator); + } + + return builder; + } +} diff --git a/service/Core/SemanticKernel/SemanticKernelConfig.cs b/service/Core/SemanticKernel/SemanticKernelConfig.cs new file mode 100644 index 000000000..b20d90059 --- /dev/null +++ b/service/Core/SemanticKernel/SemanticKernelConfig.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.KernelMemory.SemanticKernel; + +/// +/// Semantic Kernel TextGenerator And TextEmbeddingGenerator Config +/// +public class SemanticKernelConfig +{ + /// + /// Max size of the LLM attention window, ie max tokens that can be processed. + /// + public int MaxTokenTotal { get; set; } = 8191; +} diff --git a/service/Core/SemanticKernel/SemanticKernelTextEmbeddingGenerator.cs b/service/Core/SemanticKernel/SemanticKernelTextEmbeddingGenerator.cs new file mode 100644 index 000000000..561181017 --- /dev/null +++ b/service/Core/SemanticKernel/SemanticKernelTextEmbeddingGenerator.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.KernelMemory.AI; +using Microsoft.KernelMemory.AI.OpenAI; +using Microsoft.KernelMemory.Diagnostics; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.Embeddings; + +namespace Microsoft.KernelMemory.SemanticKernel; + +internal class SemanticKernelTextEmbeddingGenerator : ITextEmbeddingGenerator +{ + private readonly ITextEmbeddingGenerationService _service; + private readonly ITextTokenizer _tokenizer; + private readonly ILogger _log; + + /// + public int MaxTokens { get; } + + /// + public int CountTokens(string text) => this._tokenizer.CountTokens(text); + + public SemanticKernelTextEmbeddingGenerator( + ITextEmbeddingGenerationService textEmbeddingGenerationService, + SemanticKernelConfig config, + ITextTokenizer? textTokenizer = null, + ILoggerFactory? loggerFactory = null) + { + this._service = textEmbeddingGenerationService ?? throw new ArgumentNullException(nameof(textEmbeddingGenerationService)); + this.MaxTokens = config.MaxTokenTotal; + + var log = loggerFactory?.CreateLogger(); + this._log = log ?? DefaultLogger.Instance; + + if (textTokenizer == null) + { + this._log.LogWarning( + "Tokenizer not specified, will use {0}. The token count might be incorrect, causing unexpected errors", + nameof(DefaultGPTTokenizer)); + textTokenizer = new DefaultGPTTokenizer(); + } + + this._tokenizer = textTokenizer; + } + + /// + public Task GenerateEmbeddingAsync(string text, CancellationToken cancellationToken = default) + { + this._log.LogTrace("Generating embedding with SK embedding generator service"); + + return this._service.GenerateEmbeddingAsync(text, cancellationToken); + } +} diff --git a/service/Core/SemanticKernel/SemanticKernelTextGenerator.cs b/service/Core/SemanticKernel/SemanticKernelTextGenerator.cs new file mode 100644 index 000000000..1699705b4 --- /dev/null +++ b/service/Core/SemanticKernel/SemanticKernelTextGenerator.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.Logging; +using Microsoft.KernelMemory.AI; +using Microsoft.KernelMemory.AI.OpenAI; +using Microsoft.KernelMemory.Diagnostics; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.TextGeneration; + +namespace Microsoft.KernelMemory.SemanticKernel; + +internal class SemanticKernelTextGenerator : ITextGenerator +{ + private readonly ITextGenerationService _service; + private readonly ITextTokenizer _tokenizer; + private readonly ILogger _log; + + /// + public int MaxTokenTotal { get; } + + /// + public int CountTokens(string text) => this._tokenizer.CountTokens(text); + + public SemanticKernelTextGenerator( + ITextGenerationService textGenerationService, + SemanticKernelConfig config, + ITextTokenizer? textTokenizer = null, + ILoggerFactory? loggerFactory = null) + { + this._service = textGenerationService ?? throw new ArgumentNullException(nameof(textGenerationService)); + this.MaxTokenTotal = config.MaxTokenTotal; + + var log = loggerFactory?.CreateLogger(); + this._log = log ?? DefaultLogger.Instance; + + if (textTokenizer == null) + { + this._log.LogWarning( + "Tokenizer not specified, will use {0}. The token count might be incorrect, causing unexpected errors", + nameof(DefaultGPTTokenizer)); + textTokenizer = new DefaultGPTTokenizer(); + } + + this._tokenizer = textTokenizer; + } + + /// + public async IAsyncEnumerable GenerateTextAsync( + string prompt, + TextGenerationOptions options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + this._log.LogTrace("Generating text with SK text generator service"); + + var contents = this._service.GetStreamingTextContentsAsync( + prompt, ToPromptExecutionSettings(options), null, cancellationToken).ConfigureAwait(false); + + await foreach (StreamingTextContent? content in contents) + { + if (content != null) + { + yield return content.ToString(); + } + } + } + + private static PromptExecutionSettings ToPromptExecutionSettings(TextGenerationOptions options) + { + var settings = new PromptExecutionSettings + { + ExtensionData = new Dictionary() + { + [nameof(options.Temperature)] = options.Temperature, + [nameof(options.TopP)] = options.TopP, + [nameof(options.PresencePenalty)] = options.PresencePenalty, + [nameof(options.FrequencyPenalty)] = options.FrequencyPenalty, + [nameof(options.StopSequences)] = options.StopSequences, + [nameof(options.ResultsPerPrompt)] = options.ResultsPerPrompt, + [nameof(options.TokenSelectionBiases)] = options.TokenSelectionBiases + } + }; + + if (options.MaxTokens != null) + { + settings.ExtensionData[nameof(options.MaxTokens)] = options.MaxTokens; + } + + return settings; + } +} diff --git a/service/Service/Service.csproj b/service/Service/Service.csproj index 1e965c999..f1f6b5667 100644 --- a/service/Service/Service.csproj +++ b/service/Service/Service.csproj @@ -7,6 +7,7 @@ false 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 $(NoWarn);CA2007,CA1724,CA2254,CA1031,CS1591,CA1861,CA1303,SKEXP0001 + $(DefineConstants);KernelMemoryDev