From b7f7170a64ada28ddce827618e589c8a1860942f Mon Sep 17 00:00:00 2001 From: David Grieve Date: Tue, 27 Jun 2023 08:02:53 -0400 Subject: [PATCH] Java: Example14_SemanticMemory (#1355) ### Motivation and Context Port Example14_SemanticMemory.cs to Java ### Description Port of Example14_SemanticMemory.cs to Java. This sample uses an Azure Cognitive Search connector, which was also ported from dotnet/src/Connectors/Connectors.Memory.AzureCognitiveSearch Changes needed to api and core modules. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows SK Contribution Guidelines (https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) - [x] ~The code follows the .NET coding conventions (https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions) verified with `dotnet format`~ Java code follows AOSP style - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone :smile: --------- Co-authored-by: Luigi Montoya Co-authored-by: joe-braley Co-authored-by: Luigi96 Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Co-authored-by: John Oliver <1615532+johnoliver@users.noreply.github.com> --- .../e2e/AbstractKernelTest.java | 19 +- .../e2e/TextEmbeddingsTest.java | 10 +- java/samples/pom.xml | 107 +----- java/samples/sample-code/pom.xml | 15 +- .../com/microsoft/semantickernel/Config.java | 19 +- .../Example14_SemanticMemory.java | 174 +++++++++ .../com/microsoft/semantickernel/Kernel.java | 24 +- .../semantickernel/KernelConfig.java | 25 +- .../builders/BuildersSingleton.java | 14 +- .../semantickernel/builders/SKBuilders.java | 5 + .../semantickernel/memory/MemoryStore.java | 4 + .../memory/SemanticTextMemory.java | 4 + java/semantickernel-connectors-parent/pom.xml | 20 + .../semantickernel-connectors/pom.xml | 25 ++ .../OpenAITextEmbeddingGeneration.java | 2 +- .../AzureCognitiveSearchMemory.java | 359 ++++++++++++++++++ .../AzureCognitiveSearchMemoryException.java | 29 ++ .../AzureCognitiveSearchRecord.java | 134 +++++++ .../semantickernel/DefaultKernel.java | 45 ++- .../memory/DefaultSemanticTextMemory.java | 38 +- .../memory/MemoryConfiguration.java | 5 +- .../memory/VolatileMemoryStore.java | 13 +- java/spotbugs-exclude.xml | 20 +- 23 files changed, 922 insertions(+), 188 deletions(-) create mode 100644 java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example14_SemanticMemory.java create mode 100644 java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchMemory.java create mode 100644 java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchMemoryException.java create mode 100644 java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchRecord.java diff --git a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/AbstractKernelTest.java b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/AbstractKernelTest.java index 565a85621b..515a839e74 100644 --- a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/AbstractKernelTest.java +++ b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/AbstractKernelTest.java @@ -1,13 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.e2e; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Properties; - -import org.junit.jupiter.api.condition.EnabledIf; - import com.azure.ai.openai.OpenAIAsyncClient; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.models.NonAzureOpenAIKeyCredential; @@ -18,6 +11,12 @@ import com.microsoft.semantickernel.builders.SKBuilders; import com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion; import com.microsoft.semantickernel.memory.VolatileMemoryStore; import com.microsoft.semantickernel.textcompletion.TextCompletion; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; @EnabledIf("isAzureTestEnabled") public class AbstractKernelTest { @@ -27,10 +26,14 @@ public class AbstractKernelTest { public static Kernel buildTextCompletionKernel() throws IOException { String model = "text-davinci-003"; - TextCompletion textCompletion = new OpenAITextCompletion(getAzureOpenAIClient(), model); + final OpenAIAsyncClient openAIClient = getAzureOpenAIClient(); + TextCompletion textCompletion = new OpenAITextCompletion(openAIClient, model); KernelConfig kernelConfig = SKBuilders.kernelConfig() .addTextCompletionService(model, kernel -> textCompletion) + .addTextEmbeddingsGenerationService( + model, + kernel -> SKBuilders.textEmbeddingGenerationService().build(openAIClient, model)) .build(); return SKBuilders.kernel() diff --git a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/TextEmbeddingsTest.java b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/TextEmbeddingsTest.java index 207ea03433..2220e4efc5 100644 --- a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/TextEmbeddingsTest.java +++ b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/TextEmbeddingsTest.java @@ -53,9 +53,9 @@ public class TextEmbeddingsTest extends AbstractKernelTest { LOGGER.info(String.valueOf(embeddingGeneration.generateEmbeddingsAsync(data).block())); } - @Test - @EnabledIf("isAzureTestEnabled") - public void testMemory() throws IOException { + @Test + @EnabledIf("isAzureTestEnabled") + public void testMemory() throws IOException { Kernel kernel = buildTextCompletionKernel(); kernel.importSkill(new TextMemorySkill(), "aboutMe"); @@ -96,8 +96,8 @@ public class TextEmbeddingsTest extends AbstractKernelTest { kernel.getSkills()); context.getSemanticMemory() - .saveInformationAsync("aboutMe", "My name is Andrea", "fact1", null, null) - .block(); + .saveInformationAsync("aboutMe", "My name is Andrea", "fact1", null, null) + .block(); context.getSemanticMemory() .saveInformationAsync( diff --git a/java/samples/pom.xml b/java/samples/pom.xml index 8df4f20c6e..5eb1f46b66 100644 --- a/java/samples/pom.xml +++ b/java/samples/pom.xml @@ -19,8 +19,9 @@ 2.20.0 3.11.0 17 - UTF-8 + 17 17 + UTF-8 @@ -34,108 +35,4 @@ - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven.compiler-plugin.version} - - ${maven.compiler.release} - ${maven.compiler.release} - ${maven.compiler.release} - - - - - - - - com.microsoft.semantickernel - semantickernel-api - - - com.microsoft.semantickernel.connectors - semantickernel-connectors - - - com.microsoft.semantickernel - semantickernel-core - runtime - - - com.microsoft.semantickernel - semantickernel-core-skills - ${project.version} - - - org.apache.logging.log4j - log4j-api - ${log4j2.version} - - - org.apache.logging.log4j - log4j-core - ${log4j2.version} - - - org.apache.logging.log4j - log4j-slf4j2-impl - ${log4j2.version} - - - com.microsoft.semantickernel.extensions - sequentialplanner-extensions - compile - - - - - - bug-check - - - false - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven.compiler.plugin.version} - - ${maven.compiler.release} - ${maven.compiler.release} - ${project.build.sourceEncoding} - true - - -XDcompilePolicy=simple - - - -Xplugin:ErrorProne - -XepOpt:NullAway:AnnotatedPackages=com.microsoft.semantickernel - -Xep:AlmostJavadoc:OFF -Xep:MissingSummary:OFF - -Xep:UnusedVariable:OFF -Xep:EmptyBlockTag:OFF - - - - - com.google.errorprone - error_prone_core - ${google.errorprone.core.version} - - - com.uber.nullaway - nullaway - ${com.uber.nullaway.version} - - - - - - - - diff --git a/java/samples/sample-code/pom.xml b/java/samples/sample-code/pom.xml index ebe2bf2e77..02a752881c 100644 --- a/java/samples/sample-code/pom.xml +++ b/java/samples/sample-code/pom.xml @@ -26,7 +26,8 @@ com.microsoft.semantickernel semantickernel-core - runtime + + com.microsoft.semantickernel @@ -54,5 +55,17 @@ compile + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + \ No newline at end of file diff --git a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Config.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Config.java index aa2ffd34c8..6b3c792d7b 100644 --- a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Config.java +++ b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Config.java @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel; -import java.io.IOException; - import com.azure.ai.openai.OpenAIAsyncClient; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.models.NonAzureOpenAIKeyCredential; @@ -11,16 +9,18 @@ import com.microsoft.semantickernel.util.AIProviderSettings; import com.microsoft.semantickernel.util.AzureOpenAISettings; import com.microsoft.semantickernel.util.OpenAISettings; +import java.io.IOException; + public class Config { - public static final String CONF_PROPERTIES = "java/samples/conf.properties"; + public static final String CONF_PROPERTIES = + System.getProperty("CONF_PROPERTIES", "java/samples/conf.properties"); public enum ClientType { OPEN_AI { @Override public OpenAIAsyncClient getClient() throws IOException { OpenAISettings settings = AIProviderSettings.getOpenAISettingsFromFile(CONF_PROPERTIES); - return new OpenAIClientBuilder() .credential(new NonAzureOpenAIKeyCredential(settings.getKey())) .buildAsyncClient(); @@ -32,16 +32,13 @@ public class Config { throws IOException { AzureOpenAISettings settings = AIProviderSettings.getAzureOpenAISettingsFromFile(CONF_PROPERTIES); - return new OpenAIClientBuilder().endpoint(settings.getEndpoint()) - .credential(new AzureKeyCredential(settings.getKey())).buildAsyncClient(); + return new OpenAIClientBuilder() + .endpoint(settings.getEndpoint()) + .credential(new AzureKeyCredential(settings.getKey())) + .buildAsyncClient(); } }; - /** - * Returns the client that will handle AzureOpenAI or OpenAI requests. - * - * @return client to be used by the kernel. - */ public abstract OpenAIAsyncClient getClient() throws IOException; } } diff --git a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example14_SemanticMemory.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example14_SemanticMemory.java new file mode 100644 index 0000000000..171f307837 --- /dev/null +++ b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example14_SemanticMemory.java @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.syntaxexamples; + +import com.microsoft.semantickernel.Config; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.builders.SKBuilders; +import com.microsoft.semantickernel.connectors.memory.azurecognitivesearch.AzureCognitiveSearchMemory; +import com.microsoft.semantickernel.memory.SemanticTextMemory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + + +/* The files contain two examples about SK Semantic Memory. + * + * 1. Memory using Azure Cognitive Search. + * 2. Memory using a custom embedding generator and vector engine. + * + * Semantic Memory allows to store your data like traditional DBs, + * adding the ability to query it using natural language. + */ + +// ReSharper disable once InconsistentNaming +public class Example14_SemanticMemory +{ + private static final String MEMORY_COLLECTION_NAME = "SKGitHub"; + + public static void main(String[] args) throws Exception + { + System.out.println("=============================================================="); + System.out.println("======== Semantic Memory using Azure Cognitive Search ========"); + System.out.println("=============================================================="); + + /* This example leverages Azure Cognitive Search to provide SK with Semantic Memory. + * + * Azure Cognitive Search automatically indexes your data semantically, so you don't + * need to worry about embedding generation. + */ + var kernelConfig = SKBuilders.kernelConfig().build(); + + var kernelWithACS = SKBuilders.kernel() + .setKernelConfig(kernelConfig) + .withMemory(new AzureCognitiveSearchMemory(System.getenv("ACS_ENDPOINT"), System.getenv("ACS_API_KEY"))) + .build(); + + runExampleAsync(kernelWithACS).block(); + + System.out.println("===================================================="); + System.out.println("======== Semantic Memory (volatile, in RAM) ========"); + System.out.println("===================================================="); + + /* You can build your own semantic memory combining an Embedding Generator + * with a Memory storage that supports search by similarity (ie semantic search). + * + * In this example we use a volatile memory, a local simulation of a vector DB. + * + * You can replace VolatileMemoryStore with Qdrant (see QdrantMemoryStore connector) + * or implement your connectors for Pinecone, Vespa, Postgres + pgvector, SQLite VSS, etc. + */ + var openAIAsyncClient = Config.ClientType.AZURE_OPEN_AI.getClient(); + + var kernelConfigWithTextEmbedding = SKBuilders.kernelConfig() + .addTextEmbeddingsGenerationService( + "ada", + kernel -> SKBuilders.textEmbeddingGenerationService().build(openAIAsyncClient, "text-embedding-ada-002")) + .build(); + + var kernelWithCustomDb = SKBuilders.kernel() + .setKernelConfig(kernelConfigWithTextEmbedding) + .withMemoryStore(SKBuilders.memoryStore().build()) + .build(); + + runExampleAsync(kernelWithCustomDb).block(); + } + + public static Mono runExampleAsync(Kernel kernel) + { + return storeMemoryAsync(kernel) + .then(searchMemoryAsync(kernel, "How do I get started?")) + + /* + Output: + + Query: How do I get started? + + Result 1: + URL: : https://github.com/microsoft/semantic-kernel/blob/main/README.md + Title : README: Installation, getting started, and how to contribute + + Result 2: + URL: : https://github.com/microsoft/semantic-kernel/blob/main/samples/dotnet-jupyter-notebooks/00-getting-started.ipynb + Title : Jupyter notebook describing how to get started with the Semantic Kernel + + */ + + .then(searchMemoryAsync(kernel, "Can I build a chat with SK?")); + + /* + Output: + + Query: Can I build a chat with SK? + + Result 1: + URL: : https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT + Title : Sample demonstrating how to create a chat skill interfacing with ChatGPT + + Result 2: + URL: : https://github.com/microsoft/semantic-kernel/blob/main/samples/apps/chat-summary-webapp-react/README.md + Title : README: README associated with a sample chat summary react-based webapp + + */ + } + + private static Mono searchMemoryAsync(Kernel kernel, String query) + { + return kernel.getMemory().searchAsync(MEMORY_COLLECTION_NAME, query, 2, 0.5, false) + .mapNotNull(memories -> { + System.out.println("\nQuery: " + query + "\n"); + for (int n = 0; n < memories.size(); n++) { + var memory = memories.get(n); + System.out.println("Result " + (n + 1) + ":"); + System.out.println(" URL: : " + memory.getMetadata().getId()); + System.out.println(" Title : " + memory.getMetadata().getDescription()); + System.out.println(); + } + System.out.println("----------------------"); + return null; + }); + } + + private static Mono storeMemoryAsync(Kernel kernel) + { + /* Store some data in the semantic memory. + * + * When using Azure Cognitive Search the data is automatically indexed on write. + * + * When using the combination of VolatileStore and Embedding generation, SK takes + * care of creating and storing the index + */ + + return Flux.fromIterable(sampleData().entrySet()) + .doFirst(() -> System.out.println("\nAdding some GitHub file URLs and their descriptions to the semantic memory.")) + .map(entry -> { + System.out.println("Save '" + entry.getKey() + "' to memory."); + return kernel.getMemory().saveReferenceAsync( + MEMORY_COLLECTION_NAME, + entry.getValue(), + entry.getKey(), + "GitHub", + entry.getValue(), + null); + } + ) + .mapNotNull(Mono::block) + .doFinally(signalType -> System.out.println("\n----------------------")) + .then(); + } + + private static Map sampleData() + { + return Arrays.stream(new String[][] { + {"https://github.com/microsoft/semantic-kernel/blob/main/README.md", "README: Installation, getting started, and how to contribute"}, + {"https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/02-running-prompts-from-file.ipynb", "Jupyter notebook describing how to pass prompts from a file to a semantic skill or function"}, + {"https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/00-getting-started.ipynb", "Jupyter notebook describing how to get started with the Semantic Kernel"}, + {"https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT", "Sample demonstrating how to create a chat skill interfacing with ChatGPT"}, + {"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/VolatileMemoryStore.cs", "C# class that defines a volatile embedding store"}, + {"https://github.com/microsoft/semantic-kernel/blob/main/samples/dotnet/KernelHttpServer/README.md", "README: How to set up a Semantic Kernel Service API using Azure Function Runtime v4"}, + {"https://github.com/microsoft/semantic-kernel/blob/main/samples/apps/chat-summary-webapp-react/README.md", "README: README associated with a sample chat summary react-based webapp"}, + }).collect(Collectors.toMap(element -> element[0], element -> element[1])); + } +} diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/Kernel.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/Kernel.java index 82b7763c5c..2fa469bc5b 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/Kernel.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/Kernel.java @@ -32,23 +32,16 @@ public interface Kernel { /** * Reference to the engine rendering prompt templates * - * @return + * @return Reference to the engine rendering prompt templates */ PromptTemplateEngine getPromptTemplateEngine(); /** - * Return the memory store used by the kernel. + * Get the SemanticTextMemory in use. * - * @return the MemoryStore instance + * @return the SemanticTextMemory in use */ - MemoryStore getMemoryStore(); - - /** - * Set the SemanticTextMemory to use. - * - * @param memory {@link SemanticTextMemory} instance - */ - void registerMemory(SemanticTextMemory memory); + SemanticTextMemory getMemory(); /** * Run a pipeline composed of synchronous and asynchronous functions. @@ -129,6 +122,7 @@ public interface Kernel { @Nullable private KernelConfig kernelConfig = null; @Nullable private PromptTemplateEngine promptTemplateEngine = null; @Nullable private MemoryStore memoryStore = null; + @Nullable private SemanticTextMemory memory = null; public Builder setKernelConfig(KernelConfig kernelConfig) { this.kernelConfig = kernelConfig; @@ -145,6 +139,11 @@ public interface Kernel { return this; } + public Builder withMemory(SemanticTextMemory memory) { + this.memory = memory; + return this; + } + public Kernel build() { if (kernelConfig == null) { throw new IllegalStateException("Must provide a kernel configuration"); @@ -152,7 +151,7 @@ public interface Kernel { return BuildersSingleton.INST .getKernelBuilder() - .build(kernelConfig, promptTemplateEngine, memoryStore); + .build(kernelConfig, promptTemplateEngine, memory, memoryStore); } } @@ -160,6 +159,7 @@ public interface Kernel { Kernel build( KernelConfig kernelConfig, @Nullable PromptTemplateEngine promptTemplateEngine, + @Nullable SemanticTextMemory memory, @Nullable MemoryStore memoryStore); } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelConfig.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelConfig.java index c68a8ad8f3..31a95ba8c1 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelConfig.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelConfig.java @@ -6,10 +6,13 @@ import com.microsoft.semantickernel.chatcompletion.ChatCompletion; import com.microsoft.semantickernel.orchestration.SKFunction; import com.microsoft.semantickernel.textcompletion.TextCompletion; -import java.util.*; -import java.util.function.Function; - import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; public final class KernelConfig { @@ -73,6 +76,22 @@ public final class KernelConfig { return this.chatCompletionServices.get(serviceId); } + public Function> getTextEmbeddingGenerationServiceOrDefault( + @Nullable String serviceId) { + + if (serviceId == null) { + serviceId = DEFAULT_SERVICE_ID; + } + + if (!this.textEmbeddingGenerationServices.containsKey(serviceId)) { + throw new KernelException( + KernelException.ErrorCodes.ServiceNotFound, + "A embedding generation service id '" + serviceId + "' doesn't exist"); + } + + return this.textEmbeddingGenerationServices.get(serviceId); + } + public static class Builder { private Map> textCompletionServices = new HashMap<>(); diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/BuildersSingleton.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/BuildersSingleton.java index a60e8090ac..749d5d6178 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/BuildersSingleton.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/BuildersSingleton.java @@ -4,6 +4,7 @@ package com.microsoft.semantickernel.builders; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; import com.microsoft.semantickernel.chatcompletion.ChatCompletion; +import com.microsoft.semantickernel.memory.MemoryStore; import com.microsoft.semantickernel.memory.SemanticTextMemory; import com.microsoft.semantickernel.orchestration.ContextVariables; import com.microsoft.semantickernel.orchestration.SKContext; @@ -47,6 +48,9 @@ public enum BuildersSingleton { private static final String FALLBACK_CHAT_COMPLETION_BUILDER_CLASS = "com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatCompletion$Builder"; + private static final String FALLBACK_MEMORY_STORE_BUILDER_CLASS = + "com.microsoft.semantickernel.memory.VolatileMemoryStore$Builder"; + private final FunctionBuilders functionBuilders; private final Kernel.InternalBuilder kernelBuilder; private final TextCompletion.Builder textCompletionBuilder; @@ -57,8 +61,8 @@ public enum BuildersSingleton { private final SKContext.Builder context; private final PromptTemplateEngine.Builder promptTemplateEngine; private final ChatCompletion.Builder chatCompletion; - private final SemanticTextMemory.Builder semanticTextMemoryBuilder; + private final MemoryStore.Builder memoryStoreBuilder; BuildersSingleton() { try { @@ -108,6 +112,10 @@ public enum BuildersSingleton { ServiceLoadUtil.findServiceLoader( ChatCompletion.Builder.class, FALLBACK_CHAT_COMPLETION_BUILDER_CLASS); + memoryStoreBuilder = + ServiceLoadUtil.findServiceLoader( + MemoryStore.Builder.class, FALLBACK_MEMORY_STORE_BUILDER_CLASS); + } catch (Throwable e) { Logger LOGGER = LoggerFactory.getLogger(BuildersSingleton.class); LOGGER.error("Failed to discover Semantic Kernel Builders", e); @@ -176,4 +184,8 @@ public enum BuildersSingleton { public ChatCompletion.Builder getChatCompletion() { return chatCompletion; } + + public MemoryStore.Builder getMemoryStoreBuilder() { + return memoryStoreBuilder; + } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SKBuilders.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SKBuilders.java index 119edf95c5..95c9c17853 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SKBuilders.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SKBuilders.java @@ -5,6 +5,7 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.KernelConfig; import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; import com.microsoft.semantickernel.chatcompletion.ChatCompletion; +import com.microsoft.semantickernel.memory.MemoryStore; import com.microsoft.semantickernel.memory.SemanticTextMemory; import com.microsoft.semantickernel.orchestration.ContextVariables; import com.microsoft.semantickernel.orchestration.SKContext; @@ -82,4 +83,8 @@ public class SKBuilders { public static ChatCompletion.Builder chatCompletion() { return BuildersSingleton.INST.getChatCompletion(); } + + public static MemoryStore.Builder memoryStore() { + return BuildersSingleton.INST.getMemoryStoreBuilder(); + } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryStore.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryStore.java index 0a6013144c..22795d00ba 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryStore.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryStore.java @@ -146,4 +146,8 @@ public interface MemoryStore { @Nonnull Embedding embedding, double minRelevanceScore, boolean withEmbedding); + + interface Builder { + MemoryStore build(); + } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/SemanticTextMemory.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/SemanticTextMemory.java index 3bca7cc2b3..cd42f0a329 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/SemanticTextMemory.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/SemanticTextMemory.java @@ -67,6 +67,10 @@ public interface SemanticTextMemory { public Mono getAsync( @Nonnull String collection, @Nonnull String key, boolean withEmbedding); + @Deprecated + /** + * @deprecated No longer appears in the C# ISemanticTextMemory interface. + */ SemanticTextMemory merge(MemoryQueryResult b); /** diff --git a/java/semantickernel-connectors-parent/pom.xml b/java/semantickernel-connectors-parent/pom.xml index a3798f018c..37c1b167e4 100644 --- a/java/semantickernel-connectors-parent/pom.xml +++ b/java/semantickernel-connectors-parent/pom.xml @@ -19,4 +19,24 @@ semantickernel-connectors + + + 2.15.2 + + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/pom.xml b/java/semantickernel-connectors-parent/semantickernel-connectors/pom.xml index 9a7f4dc0cd..9e92a6f10e 100644 --- a/java/semantickernel-connectors-parent/semantickernel-connectors/pom.xml +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/pom.xml @@ -14,8 +14,33 @@ semantickernel-connectors + + + azure-sdk-for-java + https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-java/maven/v1 + + true + + + true + + + + + com.azure + azure-search-documents + 11.6.0-beta.6 + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + com.microsoft.semantickernel semantickernel-api diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/textembeddings/OpenAITextEmbeddingGeneration.java b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/textembeddings/OpenAITextEmbeddingGeneration.java index cfdf8e1a55..50b565c1d9 100644 --- a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/textembeddings/OpenAITextEmbeddingGeneration.java +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/textembeddings/OpenAITextEmbeddingGeneration.java @@ -27,7 +27,7 @@ public class OpenAITextEmbeddingGeneration extends ClientBase } protected Mono>> internalGenerateTextEmbeddingsAsync(List data) { - EmbeddingsOptions options = new EmbeddingsOptions(data).setModel(getModelId()); + EmbeddingsOptions options = new EmbeddingsOptions(data).setModel(getModelId()).setUser("default"); return getClient() .getEmbeddings(getModelId(), options) diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchMemory.java b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchMemory.java new file mode 100644 index 0000000000..0357ea4aef --- /dev/null +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchMemory.java @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.connectors.memory.azurecognitivesearch; + +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.rest.Response; +import com.azure.search.documents.SearchAsyncClient; +import com.azure.search.documents.indexes.SearchIndexAsyncClient; +import com.azure.search.documents.indexes.SearchIndexClientBuilder; +import com.azure.search.documents.indexes.models.PrioritizedFields; +import com.azure.search.documents.indexes.models.SearchField; +import com.azure.search.documents.indexes.models.SearchIndex; +import com.azure.search.documents.indexes.models.SemanticConfiguration; +import com.azure.search.documents.indexes.models.SemanticField; +import com.azure.search.documents.indexes.models.SemanticSettings; +import com.azure.search.documents.models.IndexDocumentsOptions; +import com.azure.search.documents.models.IndexDocumentsResult; +import com.azure.search.documents.models.QueryLanguage; +import com.azure.search.documents.models.QueryType; +import com.azure.search.documents.models.SearchOptions; +import com.azure.search.documents.util.SearchPagedResponse; +import com.microsoft.semantickernel.exceptions.NotSupportedException; +import com.microsoft.semantickernel.memory.MemoryQueryResult; +import com.microsoft.semantickernel.memory.MemoryRecordMetadata; +import com.microsoft.semantickernel.memory.SemanticTextMemory; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** + * Semantic Memory implementation using Azure Cognitive Search. For more information about Azure + * Cognitive Search {@see https://learn.microsoft.com/azure/search/search-what-is-azure-search} + */ +public class AzureCognitiveSearchMemory implements SemanticTextMemory { + + private static final String EMPTY_STRING = ""; + + private final SearchIndexAsyncClient _adminClient; + + private final ConcurrentMap _clientsByIndex = + new ConcurrentHashMap<>(); + + /** + * Create a new instance of semantic memory using Azure Cognitive Search. + * + * @param endpoint Azure Cognitive Search URI, e.g. {@code "https://contoso.search.windows.net"} + * @param apiKey The key used to authenticate HTTP requests sent to the Azure Cognitive Search service. + */ + public AzureCognitiveSearchMemory(String endpoint, String apiKey) { + AzureKeyCredential credentials = new AzureKeyCredential(apiKey); + this._adminClient = + new SearchIndexClientBuilder() + .endpoint(endpoint) + .credential(credentials) + .buildAsyncClient(); + } + + /** + * Create a new instance of semantic memory using Azure Cognitive Search. + * + * @param endpoint Azure Cognitive Search URI, e.g. {@code "https://contoso.search.windows.net"} + * @param credentials TokenCredential used to authorize requests sent to the Azure Cognitive Search service. + */ + public AzureCognitiveSearchMemory(String endpoint, TokenCredential credentials) { + this._adminClient = + new SearchIndexClientBuilder() + .endpoint(endpoint) + .credential(credentials) + .buildAsyncClient(); + } + + private AzureCognitiveSearchMemory(AzureCognitiveSearchMemory other) { + this._adminClient = other._adminClient; + } + + @Override + public AzureCognitiveSearchMemory copy() { + return new AzureCognitiveSearchMemory(this); + } + + @Override + public Mono saveInformationAsync( + @Nonnull String collection, + @Nonnull String text, + @Nonnull String id, + @Nullable String description, + @Nullable String additionalMetadata) { + collection = normalizeIndexName(collection); + + AzureCognitiveSearchRecord record = + new AzureCognitiveSearchRecord( + encodeId(id), text, description, additionalMetadata, null, false); + return this.upsertRecordAsync(collection, record); + } + + @Override + public Mono saveReferenceAsync( + @Nonnull String collection, + @Nonnull String text, + @Nonnull String externalId, + @Nonnull String externalSourceName, + @Nullable String description, + @Nullable String additionalMetadata) { + collection = normalizeIndexName(collection); + + AzureCognitiveSearchRecord record = + new AzureCognitiveSearchRecord( + encodeId(externalId), + text, + description, + additionalMetadata, + externalSourceName, + true); + return this.upsertRecordAsync(collection, record); + } + + @Override + public Mono getAsync( + @Nonnull String collection, @Nonnull String key, boolean withEmbedding) { + + SearchAsyncClient client = this.getSearchClient(normalizeIndexName(collection)); + + return client.getDocumentWithResponse(encodeId(key), AzureCognitiveSearchRecord.class, null) + .map( + (response) -> { + if (response.getStatusCode() == 404 || response.getValue() == null) { + throw new AzureCognitiveSearchMemoryException( + "Memory read returned null"); + } + return new MemoryQueryResult( + toMemoryRecordMetadata(response.getValue()), 1); + }); + } + + @Override + public SemanticTextMemory merge(MemoryQueryResult b) { + throw new NotSupportedException("Deprecated"); + } + + @Override + public Mono removeAsync(@Nonnull String collection, @Nonnull String key) { + + SearchAsyncClient client = this.getSearchClient(normalizeIndexName(collection)); + + List records = + Collections.singletonList( + new AzureCognitiveSearchRecord(encodeId(key), "", "", "", "", false)); + + return client.deleteDocumentsWithResponse(records, null).then(); + } + + private Collector> toMemoryQueryResultList = + Collector.of( + ArrayList::new, + (list, response) -> { + if (response.getStatusCode() == 404 || response.getValue() == null) { + throw new AzureCognitiveSearchMemoryException( + "Memory read returned null"); + } + response.getValue().stream() + .map( + searchResult -> + new MemoryQueryResult( + toMemoryRecordMetadata( + searchResult.getDocument( + AzureCognitiveSearchRecord + .class)), + searchResult.getScore())) + .forEach(list::add); + }, + (left, right) -> { + left.addAll(right); + return left; + }); + + @Override + public Mono> searchAsync( + @Nonnull String collection, + @Nonnull String query, + int limit, + double minRelevanceScore, + boolean withEmbeddings) { + + SearchAsyncClient client = this.getSearchClient(normalizeIndexName(collection)); + + SearchOptions options = + new SearchOptions() + .setQueryType(QueryType.SEMANTIC) + .setSemanticConfigurationName("default") + .setQueryLanguage(QueryLanguage.EN_US) + .setAnswersCount(limit); + + return client.search(query, options).byPage().collect(toMemoryQueryResultList); + } + + @Override + public Mono> getCollectionsAsync() { + return this._adminClient + .listIndexes() + .map(SearchIndex::getName) + .collect(Collectors.toList()); + } + + // Index names cannot contain special chars. We use this rule to replace a few common ones + // with an underscore and reduce the chance of errors. If other special chars are used, we leave + // it + // to the service to throw an error. + // Note: + // - replacing chars introduces a small chance of conflicts, e.g. "the-user" and "the_user". + // - we should consider whether making this optional and leave it to the developer to handle. + // + // Java 8 does not support \s for whitespace, so we have to list them all. + private static String whitespace = " |\\t|\\n|\\x0B|\\f|\\r"; + private static String s_replaceIndexNameSymbolsRegex = "[" + whitespace + "|\\|/|.|_|:]"; + + // Get a search client for the index specified. + // Note: the index might not exist, but we avoid checking everytime and the extra latency. + // Index name + // Search client ready to read/write + private SearchAsyncClient getSearchClient(String indexName) { + // Search an available client from the local cache + String normalizeIndexName = normalizeIndexName(indexName); + return _clientsByIndex.computeIfAbsent( + indexName, k -> _adminClient.getSearchAsyncClient(normalizeIndexName)); + } + + // Create a new search index. + // Index name + // Task cancellation token + // private Mono createIndexAsync(String indexName) { + private Mono createIndexAsync(String indexName) { + + indexName = normalizeIndexName(indexName); + + // TODO: Use FieldBuilder pending resolution of + // https://github.com/Azure/azure-sdk-for-java/issues/35584 + // List fields = + // FieldBuilder.build(AzureCognitiveSearchRecord.class, null); + List fields = AzureCognitiveSearchRecord.searchFields(); + + SemanticField titleField = new SemanticField().setFieldName("Description"); + + List prioritizedContentFields = + Arrays.asList( + new SemanticField().setFieldName("Text"), + new SemanticField().setFieldName("AdditionalMetadata")); + + PrioritizedFields prioritizedFields = + new PrioritizedFields() + .setTitleField(titleField) + .setPrioritizedContentFields(prioritizedContentFields); + + SemanticConfiguration semanticConfiguration = + new SemanticConfiguration("default", prioritizedFields); + + SemanticSettings semanticSettings = + new SemanticSettings() + .setConfigurations( + Collections.singletonList(semanticConfiguration) + ); + + SearchIndex newIndex = + new SearchIndex(indexName, fields) + .setSemanticSettings(semanticSettings); + + return this._adminClient.createIndex(newIndex); + } + + @SuppressWarnings("unchecked") + private Mono upsertRecordAsync(String indexName, AzureCognitiveSearchRecord record) { + SearchAsyncClient client = this.getSearchClient(indexName); + IndexDocumentsOptions throwOnAnyError = + new IndexDocumentsOptions().setThrowOnAnyError(true); + + Function, String> transform = + (response) -> { + if (response.getStatusCode() == 404 + || response.getValue() == null + || response.getValue().getResults() == null + || response.getValue().getResults().isEmpty()) { + throw new AzureCognitiveSearchMemoryException( + "Memory write returned null or an empty set"); + } + return response.getValue().getResults().get(0).getKey(); + }; + + return client.mergeOrUploadDocumentsWithResponse(Collections.singleton(record), throwOnAnyError) + .map(transform) + .onErrorResume(e -> + createIndexAsync(indexName) + .then(client.mergeOrUploadDocumentsWithResponse( + Collections.singleton(record), throwOnAnyError)) + .map(transform) + ); + + } + + private static MemoryRecordMetadata toMemoryRecordMetadata(AzureCognitiveSearchRecord data) { + return new MemoryRecordMetadata( + data.isReference(), + decodeId(data.getId()), + normalizeNullToEmptyString(data.getText()), + normalizeNullToEmptyString(data.getDescription()), + normalizeNullToEmptyString(data.getExternalSourceName()), + normalizeNullToEmptyString(data.getAdditionalMetadata())); + } + + private static String normalizeNullToEmptyString(@Nullable String value) { + return value != null ? value : EMPTY_STRING; + } + + // Normalize index name to match ACS rules. + // The method doesn't handle all the error scenarios, leaving it to the service + // to throw an error for edge cases not handled locally. + // Value to normalize + // Normalized name + private static String normalizeIndexName(String indexName) { + if (indexName.length() > 128) { + throw new AzureCognitiveSearchMemoryException( + "The collection name is too long, it cannot exceed 128 chars"); + } + + indexName = indexName.toLowerCase(Locale.ROOT); + return indexName.replaceAll(s_replaceIndexNameSymbolsRegex, "-"); + } + + // ACS keys can contain only letters, digits, underscore, dash, equal sign, recommending + // to encode values with a URL-safe algorithm. + // Original Id + // Encoded id + private static String encodeId(@Nullable String realId) { + if (realId == null) { + return EMPTY_STRING; + } + byte[] bytes = Base64.getUrlEncoder().encode(realId.getBytes(StandardCharsets.UTF_8)); + return new String(bytes, StandardCharsets.UTF_8); + } + + private static String decodeId(@Nullable String encodedId) { + if (encodedId == null) { + return EMPTY_STRING; + } + byte[] bytes = Base64.getUrlDecoder().decode(encodedId.getBytes(StandardCharsets.UTF_8)); + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchMemoryException.java b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchMemoryException.java new file mode 100644 index 0000000000..105ca55b59 --- /dev/null +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchMemoryException.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.connectors.memory.azurecognitivesearch; + +import com.microsoft.semantickernel.SKException; + +import javax.annotation.Nullable; + +/** Exception thrown by the Azure Cognitive Search connector */ +public class AzureCognitiveSearchMemoryException extends SKException { + /** + * Initializes a new instance of the {@code AzureCognitiveSearchMemoryException} class. + * + * @param message Exception message. + */ + public AzureCognitiveSearchMemoryException(@Nullable String message) { + super(message); + } + + /** + * Initializes a new instance of the {@code AzureCognitiveSearchMemoryException} class. + * + * @param message Exception message. + * @param cause Cause of the exception, if any. + */ + public AzureCognitiveSearchMemoryException( + @Nullable String message, @Nullable Throwable cause) { + super(message, cause); + } +} diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchRecord.java b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchRecord.java new file mode 100644 index 0000000000..58b0fa409f --- /dev/null +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/memory/azurecognitivesearch/AzureCognitiveSearchRecord.java @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.connectors.memory.azurecognitivesearch; + +import com.azure.search.documents.indexes.SearchableField; +import com.azure.search.documents.indexes.SimpleField; +import com.azure.search.documents.indexes.models.LexicalAnalyzerName; +import com.azure.search.documents.indexes.models.SearchField; +import com.azure.search.documents.indexes.models.SearchFieldDataType; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Arrays; +import java.util.List; + + +/** + * Azure Cognitive Search record and index definition. Note: once defined, index cannot be modified. + */ +public final class AzureCognitiveSearchRecord { + @SimpleField(isKey = true, isFilterable = false) + private final String id; + + @SearchableField(analyzerName = "standard.lucene") + private final String text; + + @SearchableField(analyzerName = "standard.lucene") + private final String description; + + @SearchableField(analyzerName = "standard.lucene") + private final String additionalMetadata; + + @SimpleField(isFilterable = false) + private final String externalSourceName; + + @SimpleField(isFilterable = false) + private final boolean isReference; + + @JsonCreator + public AzureCognitiveSearchRecord( + @JsonProperty("Id") String id, + @JsonProperty("Text") String text, + @JsonProperty("Description") String description, + @JsonProperty("AdditionalMetadata") String additionalMetadata, + @JsonProperty("ExternalSourceName") String externalSourceName, + @JsonProperty("Reference") boolean isReference) { + this.id = id; + this.text = text; + this.description = description; + this.additionalMetadata = additionalMetadata; + this.externalSourceName = externalSourceName; + this.isReference = isReference; + } + + /** + * Record ID. The record is not filterable to save quota, also SK uses only semantic search. + * + * @return Record ID. + */ + public String getId() { + return id; + } + + /** + * Content is stored here. + * + * @return Content is stored here, {@code null} if not set. + */ + public String getText() { + return text; + } + + /** + * Optional description of the content, e.g. a title. This can be useful when indexing external + * data without pulling in the entire content. + * + * @return Optional description of the content, {@code null} if not set. + */ + public String getDescription() { + return description; + } + + /** + * Additional metadata. Currently, this is a String where you could store serialized data as + * JSON. In future the design might change to allow storing named values and leverage filters. + * + * @return Additional metadata, {@code null} if not set. + */ + public String getAdditionalMetadata() { + return additionalMetadata; + } + + /** + * Name of the external source, in cases where the content and the Id are referenced to external + * information. + * + * @return Name of the external source, in cases where the content and the Id are referenced to + * external information, {@code null} if not set. + */ + public String getExternalSourceName() { + return externalSourceName; + } + + /** + * Whether the record references external information. + * + * @return {@code true} if the record references external information, {@code false} otherwise. + */ + public boolean isReference() { + return isReference; + } + + // TODO: Potentially remove after resolution of + // https://github.com/Azure/azure-sdk-for-java/issues/35584 + static List searchFields() { + return Arrays.asList( + new SearchField("Id", SearchFieldDataType.STRING).setKey(true).setFilterable(false), + new SearchField("Text", SearchFieldDataType.STRING) + .setSearchable(true) + .setAnalyzerName(LexicalAnalyzerName.EN_LUCENE), + new SearchField("Description", SearchFieldDataType.STRING) + .setSearchable(true) + .setAnalyzerName(LexicalAnalyzerName.EN_LUCENE), + new SearchField("AdditionalMetadata", SearchFieldDataType.STRING) + .setSearchable(true) + .setAnalyzerName(LexicalAnalyzerName.EN_LUCENE), + new SearchField("ExternalSourceName", SearchFieldDataType.STRING) + .setSearchable(true) + .setAnalyzerName(LexicalAnalyzerName.EN_LUCENE), + new SearchField("Reference", SearchFieldDataType.BOOLEAN).setFilterable(true) + ); + } + // TODO: add one more field with the vector, float array, mark it as searchable + +} diff --git a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernel.java b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernel.java index bdee52acc9..ef961886f1 100644 --- a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernel.java +++ b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernel.java @@ -2,12 +2,14 @@ package com.microsoft.semantickernel; import com.microsoft.semantickernel.ai.AIException; +import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; import com.microsoft.semantickernel.builders.FunctionBuilders; import com.microsoft.semantickernel.builders.SKBuilders; import com.microsoft.semantickernel.chatcompletion.ChatCompletion; import com.microsoft.semantickernel.coreskills.SkillImporter; import com.microsoft.semantickernel.exceptions.NotSupportedException; import com.microsoft.semantickernel.exceptions.SkillsNotFoundException; +import com.microsoft.semantickernel.memory.MemoryConfiguration; import com.microsoft.semantickernel.memory.MemoryStore; import com.microsoft.semantickernel.memory.SemanticTextMemory; import com.microsoft.semantickernel.orchestration.ContextVariables; @@ -24,30 +26,27 @@ import com.microsoft.semantickernel.templateengine.DefaultPromptTemplateEngine; import com.microsoft.semantickernel.templateengine.PromptTemplateEngine; import com.microsoft.semantickernel.textcompletion.CompletionSKFunction; import com.microsoft.semantickernel.textcompletion.TextCompletion; - import jakarta.inject.Inject; - import reactor.core.publisher.Mono; -import java.util.Arrays; -import java.util.Map; -import java.util.function.Function; - import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; public class DefaultKernel implements Kernel { private final KernelConfig kernelConfig; private final DefaultSkillCollection defaultSkillCollection; private final PromptTemplateEngine promptTemplateEngine; - private MemoryStore memoryStore; + private SemanticTextMemory memory; @Inject public DefaultKernel( KernelConfig kernelConfig, PromptTemplateEngine promptTemplateEngine, - MemoryStore memoryStore) { + SemanticTextMemory memory) { if (kernelConfig == null) { throw new IllegalArgumentException(); } @@ -55,7 +54,7 @@ public class DefaultKernel implements Kernel { this.kernelConfig = kernelConfig; this.promptTemplateEngine = promptTemplateEngine; this.defaultSkillCollection = new DefaultSkillCollection(); - this.memoryStore = memoryStore; + this.memory = memory; kernelConfig.getSkills().forEach(this::registerSemanticFunction); } @@ -82,7 +81,17 @@ public class DefaultKernel implements Kernel { } return (T) factory.apply(this); - } else { + } else if (EmbeddingGeneration.class.isAssignableFrom(clazz)) { + Function> factory = + kernelConfig.getTextEmbeddingGenerationServiceOrDefault(serviceId); + if (factory == null) { + throw new KernelException( + KernelException.ErrorCodes.ServiceNotFound, + "No chat completion service available"); + } + + return (T) factory.apply(this); + }else { // TODO correct exception throw new NotSupportedException( "The kernel service collection doesn't support the type " + clazz.getName()); @@ -200,13 +209,12 @@ public class DefaultKernel implements Kernel { } @Override - public MemoryStore getMemoryStore() { - return memoryStore; + public SemanticTextMemory getMemory() { + return memory; } - @Override public void registerMemory(@Nonnull SemanticTextMemory memory) { - throw new NotSupportedException("Not implemented"); + this.memory = memory; } @Override @@ -251,6 +259,7 @@ public class DefaultKernel implements Kernel { public Kernel build( KernelConfig kernelConfig, @Nullable PromptTemplateEngine promptTemplateEngine, + @Nullable SemanticTextMemory memory, @Nullable MemoryStore memoryStore) { if (promptTemplateEngine == null) { promptTemplateEngine = new DefaultPromptTemplateEngine(); @@ -262,7 +271,13 @@ public class DefaultKernel implements Kernel { "It is required to set a kernelConfig to build a kernel"); } - return new DefaultKernel(kernelConfig, promptTemplateEngine, memoryStore); + DefaultKernel kernel = new DefaultKernel(kernelConfig, promptTemplateEngine, memory); + + if (memoryStore != null) { + MemoryConfiguration.useMemory(kernel, memoryStore, null); + } + + return kernel; } } } diff --git a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/DefaultSemanticTextMemory.java b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/DefaultSemanticTextMemory.java index 09d2b2dd9d..9a960bdd20 100644 --- a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/DefaultSemanticTextMemory.java +++ b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/DefaultSemanticTextMemory.java @@ -1,21 +1,20 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.memory; +import com.microsoft.semantickernel.ai.embeddings.Embedding; import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; import com.microsoft.semantickernel.exceptions.NotSupportedException; - import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - /// /// Implementation of ./>. /// @@ -137,19 +136,24 @@ public class DefaultSemanticTextMemory implements SemanticTextMemory { @Nullable String description, @Nullable String additionalMetadata) { - return Mono.error(new NotSupportedException("Pending implementation")); - // var embedding = await this._embeddingGenerator.GenerateEmbeddingAsync(text, - // cancellationToken: cancel); - // var data = MemoryRecord.ReferenceRecord(externalId: externalId, sourceName: - // externalSourceName, description: description, - // additionalMetadata: additionalMetadata, embedding: embedding); - // - // if (!(await this._storage.DoesCollectionExistAsync(collection, cancel))) - // { - // await this._storage.CreateCollectionAsync(collection, cancel); - // } - // - // return await this._storage.UpsertAsync(collection, record: data, cancel: cancel); + return this._storage.doesCollectionExistAsync(collection) + .map(exists -> { + if (!exists) { + return this._storage.createCollectionAsync(collection); + } + return Mono.empty(); + }) + .then(this._embeddingGenerator.generateEmbeddingsAsync(Collections.singletonList(text))) + .map(embeddings -> MemoryRecord.referenceRecord( + externalId, + externalSourceName, + description, + (Embedding) embeddings.iterator().next(), + additionalMetadata, + null, + null) + ) + .flatMap(record -> this._storage.upsertAsync(collection, record)); } @Override diff --git a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/MemoryConfiguration.java b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/MemoryConfiguration.java index e8546d145d..2e990e468a 100644 --- a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/MemoryConfiguration.java +++ b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/MemoryConfiguration.java @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.memory; +import com.microsoft.semantickernel.DefaultKernel; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; @@ -31,7 +32,7 @@ public class MemoryConfiguration { * embedding generation service */ public static void useMemory( - @Nonnull Kernel kernel, + @Nonnull DefaultKernel kernel, @Nonnull MemoryStore storage, @Nullable String embeddingsServiceId) { Objects.requireNonNull(kernel); @@ -61,7 +62,7 @@ public class MemoryConfiguration { * storage} is {@code null} */ public static void useMemory( - @Nonnull Kernel kernel, + @Nonnull DefaultKernel kernel, EmbeddingGeneration embeddingGenerator, MemoryStore storage) { Objects.requireNonNull(kernel); diff --git a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/VolatileMemoryStore.java b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/VolatileMemoryStore.java index 29e04bfad1..0241cccbb5 100644 --- a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/VolatileMemoryStore.java +++ b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/VolatileMemoryStore.java @@ -3,11 +3,11 @@ package com.microsoft.semantickernel.memory; import com.microsoft.semantickernel.ai.EmbeddingVector; import com.microsoft.semantickernel.ai.embeddings.Embedding; - import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -20,8 +20,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.ToDoubleFunction; import java.util.stream.Collectors; -import javax.annotation.Nonnull; - /** A simple volatile memory embeddings store. */ public class VolatileMemoryStore implements MemoryStore { @@ -245,6 +243,15 @@ public class VolatileMemoryStore implements MemoryStore { return collection; } + public static class Builder implements MemoryStore.Builder { + + public Builder() {} + @Override + public MemoryStore build() { + return new VolatileMemoryStore(); + } + } + /* protected boolean TryGetCollection( String name, diff --git a/java/spotbugs-exclude.xml b/java/spotbugs-exclude.xml index be34efbf85..e36250d1fe 100644 --- a/java/spotbugs-exclude.xml +++ b/java/spotbugs-exclude.xml @@ -17,14 +17,14 @@ - - + + - + @@ -36,10 +36,22 @@ - + + + + + + + + + + + + +