diff --git a/.github/_typos.toml b/.github/_typos.toml index a92576ba3..c101713b6 100644 --- a/.github/_typos.toml +++ b/.github/_typos.toml @@ -29,6 +29,7 @@ ans = "ans" # Short for answers arange = "arange" # Method in Python numpy package prompty = "prompty" # prompty is a format name. ist = "ist" # German language +Prelease = "Prelease" # Prelease is a format name. [default.extend-identifiers] ags = "ags" # Azure Graph Service diff --git a/.github/workflows/close-inactive-issues.yml b/.github/workflows/close-inactive-issues.yml index 10244c760..bbd6a87c2 100644 --- a/.github/workflows/close-inactive-issues.yml +++ b/.github/workflows/close-inactive-issues.yml @@ -10,7 +10,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v5 + - uses: actions/stale@v11 with: days-before-issue-stale: 90 days-before-issue-close: 14 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index caf569803..5fe9e853c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,11 +30,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -48,10 +48,10 @@ jobs: # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild if: ${{ matrix.language != 'java' }} - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v4 - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 if: ${{ matrix.language == 'java' }} with: java-version: 17 @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/java-build.yml b/.github/workflows/java-build.yml index d4b487cfc..bf35695ae 100644 --- a/.github/workflows/java-build.yml +++ b/.github/workflows/java-build.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Need to use JDK 11 to build for JDK 8 - name: Set JDK @@ -41,7 +41,7 @@ jobs: fi # Sets up the specified JDK version from the matrix - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: ${{ steps.set-jdk.outputs.JDK_VERSION }} distribution: microsoft @@ -56,7 +56,7 @@ jobs: run: ./mvnw -B -Pbug-check -Pcompile-jdk${{ matrix.java-versions }} test --file pom.xml # Uploads test artifacts for each JDK version - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v7 if: always() with: name: test_output_sk_jdk${{ matrix.java-versions }}u diff --git a/.github/workflows/java-integration-tests.yml b/.github/workflows/java-integration-tests.yml index a65ee5ccd..45afb219e 100644 --- a/.github/workflows/java-integration-tests.yml +++ b/.github/workflows/java-integration-tests.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Need to use JDK 11 to build for JDK 8 - name: Set JDK @@ -36,7 +36,7 @@ jobs: echo "JDK_VERSION=$version" >> $GITHUB_OUTPUT fi - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: ${{ steps.set-jdk.outputs.JDK_VERSION }} distribution: microsoft diff --git a/.github/workflows/java-publish-package.yml b/.github/workflows/java-publish-package.yml index 25afec461..38512904f 100644 --- a/.github/workflows/java-publish-package.yml +++ b/.github/workflows/java-publish-package.yml @@ -17,10 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Sets up the specified JDK version from the matrix - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 11 distribution: microsoft @@ -30,7 +30,7 @@ jobs: run: ./mvnw -B -DskipTests -Pcompile-jdk8 -P-compile-jdk17 clean deploy --file pom.xml -DaltDeploymentRepository=local::file:///tmp/target/staging-deploy - name: Upload Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Artifacts path: /tmp/target/staging-deploy diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml index 428061881..d5a48785b 100644 --- a/.github/workflows/label-issues.yml +++ b/.github/workflows/label-issues.yml @@ -13,7 +13,7 @@ jobs: permissions: issues: write steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@v9 with: github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} script: | diff --git a/.github/workflows/label-title-prefix.yml b/.github/workflows/label-title-prefix.yml index f443c9809..e84551dda 100644 --- a/.github/workflows/label-title-prefix.yml +++ b/.github/workflows/label-title-prefix.yml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@v9 name: "Issue/PR: update title" with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml index c09936256..530cb5f48 100644 --- a/.github/workflows/markdown-link-check.yml +++ b/.github/workflows/markdown-link-check.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest # check out the latest version of the code steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 # Checks the status of hyperlinks in .md files in verbose mode - name: Check links diff --git a/.github/workflows/typos.yaml b/.github/workflows/typos.yaml index d7a937bad..c6845f2ab 100644 --- a/.github/workflows/typos.yaml +++ b/.github/workflows/typos.yaml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Use custom config file uses: crate-ci/typos@master diff --git a/.gitignore b/.gitignore index e6f26223b..44c24fc06 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ __pycache__/ __pypackages__/ .pdm.toml global.json +.java-version diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 6d3a56651..d58dfb70b 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -14,5 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/.vscode/settings.json b/.vscode/settings.json index 16667e414..6f3bd17c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,6 @@ }, }, "java.debug.settings.onBuildFailureProceed": true, - "java.compile.nullAnalysis.mode": "disabled" + "java.compile.nullAnalysis.mode": "disabled", + "java.configuration.updateBuildConfiguration": "interactive" } diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e406bee..0267d08f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,78 @@ -# 1.2.1 +# 1.4.4-RC2 + +- Upgrade many dependencies to recent versions +- Migrate from "Tool calls" to "Function calls" terminology, deprecated OpenAIFunctionToolCall +- Refactored Data storage area to be more extensible and added Oracle Database support (Thank you to the contributors + from Oracle for the contribution) + +# 1.4.4-RC1 + +- Add Agent framework abstractions. +- Add ChatCompletionAgent implementation. +- Add FunctionChoiceBehavior for OpenAI, replacing the older ToolCallBehavior. + +# 1.4.3 + +- Bug fix for execution on Android (https://github.com/microsoft/semantic-kernel-java/pull/284) +- Upgrade to azure-ai-openai 1.0.0-beta.14 + +# 1.4.2 + +- Fix bug effecting using native Java methods with OpenAI tool calling + +# 1.4.1 + +- Add Otel Telemetry on function invocations +- Fix bug to add type information to OpenAI function parameters +- Improve efficiency of cosine similarity calculation +- Fix concurrency bugs on database creation +- Add sample demonstrating a text splitter for chunking text for embedding +- Add hybridSearchAsync support to Azure AI Search + +# 1.4.0 + +- Upgrade to azure-ai-openai 1.0.0-beta.12 +- Add vector stores with vector search support for Azure AI Search, Redis, JDBC with Postgres, MySQL, SQLite and HSQLDB. + Moving these features out of the experimental stage. + +# 1.3.0 + +- Added support for Json Schema to Open AI Chat Completions +- Upgraded to openai sdk 1.0.0-beta.11 +- Added convenience method `FunctionInvocation.withResultTypeAutoConversion` which sets the return type and registers a + type converter based on Jackson for the return type. +- Added localization support for error/debug messages +- Add vector search to experimental vector stores. + - Approximate vector search for Azure AI Search, Redis and JDBC with Postgres. + - Exhaustive vector search for VolatileVectorStore and default JDBC query provider, MySQL, SQLite and HSQLDB. + +### Bug Fixes + +- Fixed type converters not being passed on to be used in tool invocations + +### Breaking Changes + +- To support the new Json Schema feature, ResponseFormat has changed from an enum to a class. + +# 1.2.2 - Fix bug in `FunctionInvocation` not using per-invocation type conversion when calling `withResultType`. - Fix bug in Global Hooks not being invoked under certain circumstances. - Add fluent returns to `ChatHistory` `addXMessage` methods. - Add user agent opt-out for OpenAI requests by setting the property `semantic-kernel.useragent-disable` to `true`. - Add several convenience `invokePromptAsync` methods to `Kernel`. +- Allow Handlebars templates to call Javabean getters to extract data from invocation arguments. +- Improve thread safety of `ChatHistory`. + +#### Experimental Changes + +- Add JDBC vector store #### Non-API Changes -- Add custom type Conversion example, CustomTypes_Example +- Add custom type Conversion example, `CustomTypes_Example` +- Dependency updates and pom cleanup +- Documentation updates # 1.2.0 diff --git a/COMMUNITY.md b/COMMUNITY.md index be98d4253..7afdb02cb 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -7,22 +7,6 @@ Below are some ways that you can get involved in the SK Community. File issues, submit PRs, and provide feedback and ideas to what you'd like to see from the Semantic Kernel. We do our best to respond to each submission. -## Public Community Office Hours - -We regularly have Community Office Hours that are open to the **public** to join. - -Add Semantic Kernel events to your calendar - we're running two community calls to cater different timezones for Q&A Office Hours: -* Americas timezone: download the [calendar.ics](https://aka.ms/sk-community-calendar) file. -* Asia Pacific timezone: download the [calendar-APAC.ics](https://aka.ms/sk-community-calendar-apac) file. - -Add Semantic Kernel Development Office Hours for Python and Java to your calendar to help with development: -* Java Development Office Hours: [Java Development Office Hours](https://aka.ms/sk-java-dev-sync) -* Python Development Office Hours: [Python Development Office Hours](https://aka.ms/sk-python-dev-sync) - -If you have any questions or if you would like to showcase your project(s), please email what you'd like us to cover here: skofficehours[at]microsoft.com. - -If you are unable to make it live, all meetings will be recorded and posted online. - ## Join the conversation on Discord We have a growing and active channel on Discord where you can get help, engage in lively discussion, diff --git a/README.md b/README.md index b748c348f..27585762d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![Builds](https://github.com/microsoft/semantic-kernel-java/actions/workflows/java-build.yml/badge.svg?branch=main)](https://github.com/microsoft/semantic-kernel-java/actions/workflows/java-build.yml) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.microsoft.semantic-kernel/semantickernel-api/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.microsoft.semantic-kernel/semantickernel-api) [![License: MIT](https://img.shields.io/github/license/microsoft/semantic-kernel)](https://github.com/microsoft/semantic-kernel-java/blob/main/LICENSE) [![Discord](https://img.shields.io/discord/1063152441819942922?label=Discord&logo=discord&logoColor=white&color=d82679)](https://aka.ms/SKDiscord) @@ -51,7 +50,7 @@ in a different direction, but also to consider the impact on the larger ecosyste To learn more and get started: - Read the [documentation](https://learn.microsoft.com/en-us/semantic-kernel/overview/?tabs=Java&pivots=programming-language-java) -- Learn how to [contribute](https://learn.microsoft.com/en-us/semantic-kernel/get-started/contributing?tabs=Java&pivots=programming-language-java) to the project +- Learn how to [contribute](https://learn.microsoft.com/en-us/semantic-kernel/support/contributing?tabs=Java&pivots=programming-language-java) to the project - Join the [Discord community](https://aka.ms/SKDiscord) - Attend [regular office hours and SK community events](COMMUNITY.md) - Follow the team on our [blog](https://aka.ms/sk/blog) diff --git a/connectors/semantickernel-connectors-web-bing/pom.xml b/agents/semantickernel-agents-core/pom.xml similarity index 53% rename from connectors/semantickernel-connectors-web-bing/pom.xml rename to agents/semantickernel-agents-core/pom.xml index 93eaf28a0..d5376209b 100644 --- a/connectors/semantickernel-connectors-web-bing/pom.xml +++ b/agents/semantickernel-agents-core/pom.xml @@ -1,19 +1,17 @@ - 4.0.0 - com.microsoft.semantic-kernel semantickernel-parent - 1.0.0-rc2-SNAPSHOT + 1.5.1-SNAPSHOT ../../pom.xml - semantickernel-connectors-web-bing + semantickernel-agents-core - Semantic Kernel Bing Connector - Defines Bing connectors to be used with the Semantic Kernel + Semantic Kernel Chat Completion Agent + Chat Completion Agent for Semantic Kernel @@ -21,15 +19,17 @@ semantickernel-api - com.fasterxml.jackson.core - jackson-databind - compile + com.microsoft.semantic-kernel + semantickernel-api-builders - com.fasterxml.jackson.core - jackson-core - compile + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions - + \ No newline at end of file diff --git a/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java new file mode 100644 index 000000000..b5294fe6b --- /dev/null +++ b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.agents.chatcompletion; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.agents.AgentInvokeOptions; +import com.microsoft.semantickernel.agents.AgentResponseItem; +import com.microsoft.semantickernel.agents.AgentThread; +import com.microsoft.semantickernel.agents.KernelAgent; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.functionchoice.AutoFunctionChoiceBehavior; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.InvocationReturnMode; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateFactory; +import com.microsoft.semantickernel.services.ServiceNotFoundException; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.stream.Collectors; + +public class ChatCompletionAgent extends KernelAgent { + + private ChatCompletionAgent( + String id, + String name, + String description, + Kernel kernel, + KernelArguments kernelArguments, + InvocationContext context, + String instructions, + PromptTemplate template) { + super( + id, + name, + description, + kernel, + kernelArguments, + context, + instructions, + template); + } + + /** + * Invoke the agent with the given chat history. + * + * @param messages The chat history to process + * @param thread The agent thread to use + * @param options The options for invoking the agent + * @return A Mono containing the agent response + */ + @Override + public Mono>>> invokeAsync( + List> messages, + @Nullable AgentThread thread, + @Nullable AgentInvokeOptions options) { + return ensureThreadExistsWithMessagesAsync(messages, thread, ChatHistoryAgentThread::new) + .cast(ChatHistoryAgentThread.class) + .flatMap(agentThread -> { + // Extract the chat history from the thread + ChatHistory history = new ChatHistory( + agentThread.getChatHistory().getMessages()); + + // Invoke the agent with the chat history + return internalInvokeAsync( + history, + agentThread, + options) + .map(chatMessageContents -> chatMessageContents.stream() + .map(message -> new AgentResponseItem>(message, + agentThread)) + .collect(Collectors.toList())); + }); + } + + private Mono>> internalInvokeAsync( + ChatHistory history, + AgentThread thread, + @Nullable AgentInvokeOptions options) { + if (options == null) { + options = new AgentInvokeOptions(); + } + + final Kernel kernel = options.getKernel() != null ? options.getKernel() : this.kernel; + final KernelArguments arguments = mergeArguments(options.getKernelArguments()); + final String additionalInstructions = options.getAdditionalInstructions(); + final InvocationContext invocationContext = options.getInvocationContext() != null + ? options.getInvocationContext() + : this.invocationContext; + + try { + ChatCompletionService chatCompletionService = kernel + .getService(ChatCompletionService.class, arguments); + + PromptExecutionSettings executionSettings = invocationContext != null + && invocationContext.getPromptExecutionSettings() != null + ? invocationContext.getPromptExecutionSettings() + : arguments.getExecutionSettings() + .get(chatCompletionService.getServiceId()); + + // Build base invocation context + InvocationContext.Builder builder = InvocationContext.builder() + .withPromptExecutionSettings(executionSettings) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY); + + if (invocationContext != null) { + builder = builder + .withTelemetry(invocationContext.getTelemetry()) + .withFunctionChoiceBehavior(invocationContext.getFunctionChoiceBehavior()) + .withToolCallBehavior(invocationContext.getToolCallBehavior()) + .withContextVariableConverter(invocationContext.getContextVariableTypes()) + .withKernelHooks(invocationContext.getKernelHooks()); + } + + InvocationContext agentInvocationContext = builder.build(); + + return renderInstructionsAsync(kernel, arguments, agentInvocationContext).flatMap( + instructions -> { + // Create a new chat history with the instructions + ChatHistory chat = new ChatHistory( + instructions); + + // Add agent additional instructions + if (additionalInstructions != null) { + chat.addMessage(new ChatMessageContent<>( + AuthorRole.SYSTEM, + additionalInstructions)); + } + + // Add the chat history to the new chat + chat.addAll(history); + + // Retrieve the chat message contents asynchronously and notify the thread + if (shouldNotifyFunctionCalls(agentInvocationContext)) { + // Notify all messages including function calls + return chatCompletionService + .getChatMessageContentsAsync(chat, kernel, agentInvocationContext) + .flatMapMany(Flux::fromIterable) + .concatMap(message -> notifyThreadOfNewMessageAsync(thread, message) + .thenReturn(message)) + // Filter out function calls and their results + .filter(message -> message.getContent() != null + && message.getAuthorRole() != AuthorRole.TOOL) + .collect(Collectors.toList()); + } + + // Return chat completion messages without notifying the thread + // We shouldn't add the function call content to the thread, since + // we don't know if the user will execute the call. They should add it themselves. + return chatCompletionService.getChatMessageContentsAsync(chat, kernel, + agentInvocationContext); + }); + + } catch (ServiceNotFoundException e) { + return Mono.error(e); + } + } + + boolean shouldNotifyFunctionCalls(InvocationContext invocationContext) { + if (invocationContext == null) { + return false; + } + + if (invocationContext.getFunctionChoiceBehavior() != null && invocationContext + .getFunctionChoiceBehavior() instanceof AutoFunctionChoiceBehavior) { + return ((AutoFunctionChoiceBehavior) invocationContext.getFunctionChoiceBehavior()) + .isAutoInvoke(); + } + + if (invocationContext.getToolCallBehavior() != null) { + return invocationContext.getToolCallBehavior().isAutoInvokeAllowed(); + } + + return false; + } + + @Override + public Mono notifyThreadOfNewMessageAsync(AgentThread thread, + ChatMessageContent message) { + return Mono.defer(() -> { + return thread.onNewMessageAsync(message); + }); + } + + /** + * Builder for creating instances of ChatCompletionAgent. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder implements SemanticKernelBuilder { + private String id; + private String name; + private String description; + private Kernel kernel; + private KernelArguments kernelArguments; + private InvocationContext invocationContext; + private String instructions; + private PromptTemplate template; + + /** + * Set the ID of the agent. + * + * @param id The ID of the agent. + */ + public Builder withId(String id) { + this.id = id; + return this; + } + + /** + * Set the name of the agent. + * + * @param name The name of the agent. + */ + public Builder withName(String name) { + this.name = name; + return this; + } + + /** + * Set the description of the agent. + * + * @param description The description of the agent. + */ + public Builder withDescription(String description) { + this.description = description; + return this; + } + + /** + * Set the kernel to use for the agent. + * + * @param kernel The kernel to use. + */ + public Builder withKernel(Kernel kernel) { + this.kernel = kernel; + return this; + } + + /** + * Set the kernel arguments to use for the agent. + * + * @param KernelArguments The kernel arguments to use. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withKernelArguments(KernelArguments KernelArguments) { + this.kernelArguments = KernelArguments; + return this; + } + + /** + * Set the instructions for the agent. + * + * @param instructions The instructions for the agent. + */ + public Builder withInstructions(String instructions) { + this.instructions = instructions; + return this; + } + + /** + * Set the invocation context for the agent. + * + * @param invocationContext The invocation context to use. + */ + public Builder withInvocationContext(InvocationContext invocationContext) { + this.invocationContext = invocationContext; + return this; + } + + /** + * Set the template for the agent. + * + * @param template The template to use. + */ + public Builder withTemplate(PromptTemplate template) { + this.template = template; + return this; + } + + /** + * Build the ChatCompletionAgent instance. + * + * @return The ChatCompletionAgent instance. + */ + public ChatCompletionAgent build() { + return new ChatCompletionAgent( + id, + name, + description, + kernel, + kernelArguments, + invocationContext, + instructions, + template); + } + + /** + * Build the ChatCompletionAgent instance with the given prompt template config and factory. + * + * @param promptTemplateConfig The prompt template config to use. + * @param promptTemplateFactory The prompt template factory to use. + * @return The ChatCompletionAgent instance. + */ + public ChatCompletionAgent build(PromptTemplateConfig promptTemplateConfig, + PromptTemplateFactory promptTemplateFactory) { + return new ChatCompletionAgent( + id, + name, + description, + kernel, + kernelArguments, + invocationContext, + promptTemplateConfig.getTemplate(), + promptTemplateFactory.tryCreate(promptTemplateConfig)); + } + } +} diff --git a/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java new file mode 100644 index 000000000..6b3f62a9b --- /dev/null +++ b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.agents.chatcompletion; + +import com.microsoft.semantickernel.agents.AgentThread; +import com.microsoft.semantickernel.agents.BaseAgentThread; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.UUID; + +public class ChatHistoryAgentThread extends BaseAgentThread { + private ChatHistory chatHistory; + + /** + * Constructor for ChatHistoryAgentThread. + * + */ + public ChatHistoryAgentThread() { + this(UUID.randomUUID().toString(), new ChatHistory()); + } + + /** + * Constructor for ChatHistoryAgentThread. + * + * @param chatHistory The chat history. + */ + public ChatHistoryAgentThread(@Nullable ChatHistory chatHistory) { + this(UUID.randomUUID().toString(), chatHistory); + } + + /** + * Constructor for ChatHistoryAgentThread. + * + * @param id The ID of the thread. + * @param chatHistory The chat history. + */ + public ChatHistoryAgentThread(String id, @Nullable ChatHistory chatHistory) { + super(id); + this.chatHistory = chatHistory != null ? chatHistory : new ChatHistory(); + } + + /** + * Get the chat history. + * + * @return The chat history. + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + public ChatHistory getChatHistory() { + return chatHistory; + } + + @Override + public Mono createAsync() { + if (this.id == null) { + this.id = UUID.randomUUID().toString(); + chatHistory = new ChatHistory(); + } + return Mono.just(id); + } + + @Override + public Mono deleteAsync() { + return Mono.fromRunnable(chatHistory::clear); + } + + /** + * Create a copy of the thread. + * + * @return A new instance of the thread. + */ + @Override + public ChatHistoryAgentThread copy() { + return new ChatHistoryAgentThread(this.id, new ChatHistory(chatHistory.getMessages())); + } + + @Override + public Mono onNewMessageAsync(ChatMessageContent newMessage) { + return Mono.fromRunnable(() -> { + chatHistory.addMessage(newMessage); + }); + } + + public List> getMessages() { + return chatHistory.getMessages(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder implements SemanticKernelBuilder { + private String id; + private ChatHistory chatHistory; + + /** + * Set the ID of the thread. + * + * @param id The ID of the thread. + * @return The builder instance. + */ + public Builder withId(String id) { + this.id = id; + return this; + } + + /** + * Set the chat history. + * + * @param chatHistory The chat history. + * @return The builder instance. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withChatHistory(ChatHistory chatHistory) { + this.chatHistory = chatHistory; + return this; + } + + @Override + public ChatHistoryAgentThread build() { + return new ChatHistoryAgentThread(id, chatHistory); + } + } +} diff --git a/aiservices/google/pom.xml b/aiservices/google/pom.xml index 0ca6600ec..e136a3eb3 100644 --- a/aiservices/google/pom.xml +++ b/aiservices/google/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../../pom.xml @@ -17,7 +17,7 @@ com.google.cloud libraries-bom - 26.37.0 + 26.80.0 pom import @@ -34,6 +34,22 @@ com.microsoft.semantic-kernel semantickernel-api + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + com.microsoft.semantic-kernel + semantickernel-api-localization + com.fasterxml.jackson.core diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiService.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiService.java index 7155074f5..a65e96afb 100644 --- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiService.java +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiService.java @@ -6,10 +6,18 @@ import javax.annotation.Nullable; +/** + * Makes a Gemini service available to the Semantic Kernel. + */ public class GeminiService implements AIService { private final VertexAI client; private final String modelId; + /** + * Creates a new Gemini service. + * @param client The VertexAI client + * @param modelId The Gemini model ID + */ protected GeminiService(VertexAI client, String modelId) { this.client = client; this.modelId = modelId; @@ -27,6 +35,10 @@ public String getServiceId() { return null; } + /** + * Gets the VertexAI client. + * @return The VertexAI client + */ protected VertexAI getClient() { return client; } diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiServiceBuilder.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiServiceBuilder.java index c5d3f6584..3312299cc 100644 --- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiServiceBuilder.java +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiServiceBuilder.java @@ -7,6 +7,8 @@ /** * Builder for a Gemini service. + * @param The type of the service + * @param The type of the builder */ public abstract class GeminiServiceBuilder> implements SemanticKernelBuilder { diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiChatCompletion.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiChatCompletion.java index 8aa669f07..2bd45c022 100644 --- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiChatCompletion.java +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiChatCompletion.java @@ -16,11 +16,13 @@ import com.google.protobuf.Value; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.aiservices.google.GeminiService; +import com.microsoft.semantickernel.aiservices.google.GeminiServiceBuilder; import com.microsoft.semantickernel.aiservices.google.implementation.MonoConverter; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.exceptions.AIException; import com.microsoft.semantickernel.exceptions.SKCheckedException; import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; import com.microsoft.semantickernel.orchestration.InvocationContext; @@ -30,12 +32,12 @@ import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.semanticfunctions.InputVariable; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; -import com.microsoft.semantickernel.aiservices.google.GeminiServiceBuilder; +import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent; import java.io.IOException; import java.time.OffsetDateTime; import java.util.ArrayList; @@ -48,10 +50,18 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +/** + * A chat completion service that uses the Gemini model to generate chat completions. + */ public class GeminiChatCompletion extends GeminiService implements ChatCompletionService { private static final Logger LOGGER = LoggerFactory.getLogger(GeminiChatCompletion.class); + /** + * Constructor for {@link GeminiChatCompletion}. + * @param client The VertexAI client + * @param modelId The model ID + */ public GeminiChatCompletion(VertexAI client, String modelId) { super(client, modelId); } @@ -74,6 +84,53 @@ public Mono>> getChatMessageContentsAsync(String prom invocationContext); } + @Override + public Flux> getStreamingChatMessageContentsAsync( + ChatHistory chatHistory, + @Nullable Kernel kernel, + @Nullable InvocationContext invocationContext) { + + LOGGER.warn("Streaming has been called on GeminiChatCompletion service. " + + "This is currently not supported in Gemini. " + + "The results will be returned in a non streaming fashion."); + + return getChatMessageContentsAsync(chatHistory, kernel, invocationContext) + .flatMapIterable(chatMessageContents -> chatMessageContents) + .map(content -> { + return new GeminiStreamingChatMessageContent( + content.getAuthorRole(), + content.getContent(), + getModelId(), + content.getInnerContent(), + content.getEncoding(), + content.getMetadata(), + null, + UUID.randomUUID().toString()); + }); + } + + @Override + public Flux> getStreamingChatMessageContentsAsync(String prompt, + @Nullable Kernel kernel, @Nullable InvocationContext invocationContext) { + LOGGER.warn("Streaming has been called on GeminiChatCompletion service. " + + "This is currently not supported in Gemini. " + + "The results will be returned in a non streaming fashion."); + + return getChatMessageContentsAsync(prompt, kernel, invocationContext) + .flatMapIterable(chatMessageContents -> chatMessageContents) + .map(content -> { + return new GeminiStreamingChatMessageContent( + content.getAuthorRole(), + content.getContent(), + getModelId(), + content.getInnerContent(), + content.getEncoding(), + content.getMetadata(), + null, + UUID.randomUUID().toString()); + }); + } + @Override public Mono>> getChatMessageContentsAsync(ChatHistory chatHistory, @Nullable Kernel kernel, @Nullable InvocationContext invocationContext) { @@ -97,7 +154,8 @@ private Mono>> internalChatMessageContentsAsync( try { GenerativeModel model = getGenerativeModel(kernel, invocationContext); return MonoConverter.fromApiFuture(model.generateContentAsync(contents)) - .doOnError(e -> LOGGER.error("Error generating chat completion", e)) + .doOnError(e -> LOGGER.error( + SemanticKernelResources.getString("error.generating.chat.completion"), e)) .flatMap(result -> { // Get ChatMessageContent from the response GeminiChatMessageContent response = getGeminiChatMessageContentFromResponse( @@ -253,7 +311,8 @@ private GenerativeModel getGenerativeModel(@Nullable Kernel kernel, if (settings.getResultsPerPrompt() < 1 || settings.getResultsPerPrompt() > MAX_RESULTS_PER_PROMPT) { - throw SKCheckedException.build("Error building generative model.", + throw SKCheckedException.build( + SemanticKernelResources.getString("error.building.generative.model"), new AIException(AIException.ErrorCodes.INVALID_REQUEST, String.format( "Results per prompt must be in range between 1 and %d, inclusive.", @@ -340,6 +399,13 @@ private Tool getTool(@Nullable Kernel kernel, @Nullable ToolCallBehavior toolCal return toolBuilder.build(); } + /** + * Invoke the Gemini function call. + * @param kernel The semantic kernel + * @param invocationContext Additional context for the invocation + * @param geminiFunction The Gemini function call + * @return The result of the function call + */ public Mono performFunctionCall(@Nullable Kernel kernel, @Nullable InvocationContext invocationContext, GeminiFunctionCall geminiFunction) { if (kernel == null) { @@ -370,7 +436,7 @@ public Mono performFunctionCall(@Nullable Kernel kernel, ? new ContextVariableTypes() : invocationContext.getContextVariableTypes(); - KernelFunctionArguments.Builder arguments = KernelFunctionArguments.builder(); + KernelArguments.Builder arguments = KernelArguments.builder(); geminiFunction.getFunctionCall().getArgs().getFieldsMap().forEach((key, value) -> { arguments.withVariable(key, value.getStringValue()); }); @@ -382,6 +448,9 @@ public Mono performFunctionCall(@Nullable Kernel kernel, .map(result -> new GeminiFunctionCall(geminiFunction.getFunctionCall(), result)); } + /** + * Builder for {@link GeminiChatCompletion}. + */ public static class Builder extends GeminiServiceBuilder { @Override diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiFunctionCall.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiFunctionCall.java index fc64c8307..0a6e87d4e 100644 --- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiFunctionCall.java +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiFunctionCall.java @@ -9,6 +9,9 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +/** + * Represents a function call in Gemini. + */ public class GeminiFunctionCall { @Nonnull private final FunctionCall functionCall; @@ -17,6 +20,11 @@ public class GeminiFunctionCall { private final String pluginName; private final String functionName; + /** + * Creates a new Gemini function call. + * @param functionCall The function call + * @param functionResult The result of the function invocation + */ @SuppressFBWarnings("EI_EXPOSE_REP2") public GeminiFunctionCall( @Nonnull FunctionCall functionCall, @@ -29,19 +37,35 @@ public GeminiFunctionCall( this.functionName = name[1]; } + /** + * Gets the plugin name. + * @return The plugin name + */ public String getPluginName() { return pluginName; } + /** + * Gets the function name. + * @return The function name + */ public String getFunctionName() { return functionName; } + /** + * Gets the function call. + * @return The function call + */ @SuppressFBWarnings("EI_EXPOSE_REP") public FunctionCall getFunctionCall() { return functionCall; } + /** + * Gets the function result. + * @return The function result + */ @Nullable public FunctionResult getFunctionResult() { return functionResult; diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiRole.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiRole.java index 745907239..03983553b 100644 --- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiRole.java +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiRole.java @@ -1,6 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.aiservices.google.chatcompletion; +/** + * Represents the role of a message in a Gemini conversation. + */ public enum GeminiRole { /** * A user message is a message generated by the user. diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiStreamingChatMessageContent.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiStreamingChatMessageContent.java new file mode 100644 index 000000000..e6f0d5511 --- /dev/null +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiStreamingChatMessageContent.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.google.chatcompletion; + +import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent; +import java.nio.charset.Charset; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Represents the content of a chat message. + * + * @param The type of the inner content. + */ +public class GeminiStreamingChatMessageContent extends GeminiChatMessageContent implements + StreamingChatContent { + + private final String id; + + /** + * Creates a new instance of the {@link GeminiChatMessageContent} class. + * + * @param authorRole The author role that generated the content. + * @param content The content. + * @param modelId The model id. + * @param innerContent The inner content. + * @param encoding The encoding. + * @param metadata The metadata. + * @param id The id of the message. + * @param geminiFunctionCalls The function calls. + */ + public GeminiStreamingChatMessageContent(AuthorRole authorRole, String content, + @Nullable String modelId, @Nullable T innerContent, @Nullable Charset encoding, + @Nullable FunctionResultMetadata metadata, + @Nullable List geminiFunctionCalls, + String id) { + super(authorRole, content, modelId, innerContent, encoding, metadata, geminiFunctionCalls); + this.id = id; + } + + @Override + public String getId() { + return id; + } +} diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiXMLPromptParser.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiXMLPromptParser.java index afde9a331..e43032dc1 100644 --- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiXMLPromptParser.java +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiXMLPromptParser.java @@ -17,15 +17,26 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Parses an XML prompt for a Gemini chat. + */ public class GeminiXMLPromptParser { private static final Logger LOGGER = LoggerFactory.getLogger(GeminiXMLPromptParser.class); + /** + * Represents a parsed prompt for Gemini chat. + */ public static class GeminiParsedPrompt { private final ChatHistory chatHistory; private final List functions; + /** + * Creates a new parsed prompt. + * @param parsedChatHistory The chat history + * @param parsedFunctions The functions declarations. + */ protected GeminiParsedPrompt( ChatHistory parsedChatHistory, @Nullable List parsedFunctions) { @@ -36,10 +47,18 @@ protected GeminiParsedPrompt( this.functions = parsedFunctions; } + /** + * Gets the chat history. + * @return A copy of the chat history. + */ public ChatHistory getChatHistory() { return new ChatHistory(chatHistory.getMessages()); } + /** + * Gets the functions declarations. + * @return A copy of the functions declarations. + */ public List getFunctions() { return Collections.unmodifiableList(functions); } @@ -131,6 +150,11 @@ public ChatPromptParseVisitor reset() { } } + /** + * Create a GeminiParsedPrompt by parsing a raw prompt. + * @param rawPrompt the raw prompt to parse. + * @return The parsed prompt. + */ public static GeminiParsedPrompt parse(String rawPrompt) { ChatPromptParseVisitor visitor = ChatXMLPromptParser.parse( rawPrompt, diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/StreamingTextContent.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiStreamingTextContent.java similarity index 63% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/StreamingTextContent.java rename to aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiStreamingTextContent.java index 7fdadad79..9bf9a6fd3 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/StreamingTextContent.java +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiStreamingTextContent.java @@ -1,13 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.services.textcompletion; +package com.microsoft.semantickernel.aiservices.google.textcompletion; -import com.microsoft.semantickernel.services.StreamingKernelContent; +import com.microsoft.semantickernel.services.StreamingTextContent; +import com.microsoft.semantickernel.services.textcompletion.TextContent; import javax.annotation.Nullable; /** * StreamingTextContent is a wrapper for TextContent that allows for streaming. */ -public class StreamingTextContent extends StreamingKernelContent { +public class GeminiStreamingTextContent extends StreamingTextContent { /** * Initializes a new instance of the {@code StreamingTextContent} class with a provided text @@ -15,7 +16,7 @@ public class StreamingTextContent extends StreamingKernelContent { * * @param content The text content. */ - public StreamingTextContent(TextContent content) { + public GeminiStreamingTextContent(TextContent content) { super(content, 0, null, null); } diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiTextGenerationService.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiTextGenerationService.java index 4d7c2a254..5e5551161 100644 --- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiTextGenerationService.java +++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiTextGenerationService.java @@ -7,36 +7,48 @@ import com.google.cloud.vertexai.generativeai.GenerativeModel; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.aiservices.google.GeminiService; +import com.microsoft.semantickernel.aiservices.google.GeminiServiceBuilder; import com.microsoft.semantickernel.aiservices.google.implementation.MonoConverter; import com.microsoft.semantickernel.exceptions.AIException; import com.microsoft.semantickernel.exceptions.SKCheckedException; import com.microsoft.semantickernel.exceptions.SKException; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; -import com.microsoft.semantickernel.aiservices.google.GeminiServiceBuilder; -import com.microsoft.semantickernel.services.textcompletion.StreamingTextContent; +import com.microsoft.semantickernel.services.StreamingTextContent; import com.microsoft.semantickernel.services.textcompletion.TextContent; import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; import java.io.IOException; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +/** + * A Gemini service for text generation. + * @see TextGenerationService + */ public class GeminiTextGenerationService extends GeminiService implements TextGenerationService { + private static final Logger LOGGER = LoggerFactory.getLogger(GeminiTextGenerationService.class); + /** + * Creates a new Gemini text generation service. + * @param client The VertexAI client + * @param modelId The Gemini model ID + */ public GeminiTextGenerationService(VertexAI client, String modelId) { super(client, modelId); } + /** + * Creates a new builder for a Gemini text generation service. + * @return The builder + */ public static Builder builder() { return new Builder(); } @@ -57,7 +69,7 @@ public Flux getStreamingTextContentsAsync( return this .internalGetTextAsync(prompt, executionSettings) .flatMapMany(it -> Flux.fromStream(it.stream()) - .map(StreamingTextContent::new)); + .map(GeminiStreamingTextContent::new)); } private Mono> internalGetTextAsync(String prompt, @@ -122,8 +134,12 @@ private GenerativeModel getGenerativeModel( return modelBuilder.build(); } + /** + * Builder for a Gemini text generation service. + */ public static class Builder extends GeminiServiceBuilder { + @Override public GeminiTextGenerationService build() { if (this.client == null) { diff --git a/aiservices/huggingface/pom.xml b/aiservices/huggingface/pom.xml index 7743b9899..82f5cd52d 100644 --- a/aiservices/huggingface/pom.xml +++ b/aiservices/huggingface/pom.xml @@ -6,7 +6,7 @@ com.microsoft.semantic-kernel semantickernel-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../../pom.xml @@ -31,6 +31,14 @@ com.microsoft.semantic-kernel semantickernel-api + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + com.azure azure-core diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/HuggingFaceClient.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/HuggingFaceClient.java index 93fe64fdf..4e8f9bb4d 100644 --- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/HuggingFaceClient.java +++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/HuggingFaceClient.java @@ -19,12 +19,21 @@ import reactor.core.publisher.Mono; import javax.annotation.Nullable; +/** + * A client for the Hugging Face API. + */ public class HuggingFaceClient { private final KeyCredential key; private final String endpoint; private final HttpClient httpClient; + /** + * Creates a new Hugging Face client. + * @param key The key credential for endpoint authentication. + * @param endpoint The endpoint for the Hugging Face API. + * @param httpClient The HTTP client to use for requests. + */ public HuggingFaceClient( KeyCredential key, String endpoint, @@ -74,6 +83,12 @@ public GeneratedTextItemList( } + /** + * Gets the text contents from the Hugging Face API. + * @param modelId The model ID. + * @param textGenerationRequest The text generation request. + * @return The generated text items. + */ public Mono> getTextContentsAsync( String modelId, TextGenerationRequest textGenerationRequest) { @@ -131,10 +146,17 @@ private Mono performRequest(String modelId, return responseBody; } + /** + * Creates a new builder for a Hugging Face client. + * @return The builder + */ public static Builder builder() { return new Builder(); } + /** + * Builder for a Hugging Face client. + */ public static class Builder { @Nullable @@ -144,6 +166,10 @@ public static class Builder { @Nullable private HttpClient httpClient = null; + /** + * Builds the Hugging Face client. + * @return The client + */ public HuggingFaceClient build() { if (httpClient == null) { httpClient = HttpClient.createDefault(); @@ -160,16 +186,31 @@ public HuggingFaceClient build() { httpClient); } + /** + * Sets the key credential for the client. + * @param key The key credential + * @return The builder + */ public Builder credential(KeyCredential key) { this.key = key; return this; } + /** + * Sets the endpoint for the client. + * @param endpoint The endpoint + * @return The builder + */ public Builder endpoint(String endpoint) { this.endpoint = endpoint; return this; } + /** + * Sets the HTTP client for the client. + * @param httpClient The HTTP client + * @return The builder + */ public Builder httpClient(HttpClient httpClient) { this.httpClient = httpClient; return this; diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/GeneratedTextItem.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/GeneratedTextItem.java index 6f4125c63..12ed5be60 100644 --- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/GeneratedTextItem.java +++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/GeneratedTextItem.java @@ -8,6 +8,9 @@ import java.util.List; import javax.annotation.Nullable; +/** + * Represents a generated text item deserialized from a JSON response. + */ public class GeneratedTextItem { @Nullable @@ -18,6 +21,11 @@ public class GeneratedTextItem { @JsonProperty("details") private final TextGenerationDetails details; + /** + * Constructor used by Jackson to deserialize a generated text item. + * @param generatedText The generated text. + * @param details The details of the generation. + */ @JsonCreator public GeneratedTextItem( @JsonProperty("generated_text") @Nullable String generatedText, @@ -26,16 +34,27 @@ public GeneratedTextItem( this.details = details; } + /** + * Gets the generated text. + * @return The generated text. + */ @Nullable public String getGeneratedText() { return generatedText; } + /** + * Gets the details of the generation. + * @return The details of the generation. + */ @Nullable public TextGenerationDetails getDetails() { return details; } + /** + * Represents the details of a text generation deserialized from a JSON response. + */ public static class TextGenerationDetails { @Nullable @@ -57,6 +76,14 @@ public static class TextGenerationDetails { @JsonProperty("tokens") private final List tokens; + /** + * Constructor used by Jackson to deserialize text generation details. + * @param finishReason The reason the generation finished. + * @param generatedTokens The number of tokens generated. + * @param seed The seed used for generation. + * @param prefill The prefill tokens. + * @param tokens The generated tokens. + */ @JsonCreator public TextGenerationDetails( @JsonProperty("finish_reason") @Nullable String finishReason, @@ -79,31 +106,54 @@ public TextGenerationDetails( } } + /** + * Gets the reason the generation finished. + * @return The reason the generation finished. + */ @Nullable public String getFinishReason() { return finishReason; } + /** + * Gets the number of tokens generated. + * @return The number of tokens generated. + */ public int getGeneratedTokens() { return generatedTokens; } + /** + * Gets the seed used for generation. + * @return The seed used for generation. + */ @Nullable public Long getSeed() { return seed; } + /** + * Gets the prefill tokens. + * @return The prefill tokens. + */ @Nullable public List getPrefill() { return Collections.unmodifiableList(prefill); } + /** + * Gets the generated tokens. + * @return The generated tokens. + */ @Nullable public List getTokens() { return Collections.unmodifiableList(tokens); } } + /** + * Represents a prefill token deserialized from a JSON response. + */ public static class TextGenerationPrefillToken { @JsonProperty("id") @@ -116,6 +166,12 @@ public static class TextGenerationPrefillToken { @JsonProperty("logprob") private final double logProb; + /** + * Constructor used by Jackson to deserialize a prefill token. + * @param id The token ID. + * @param text The token text. + * @param logProb The log probability of the token. + */ @JsonCreator public TextGenerationPrefillToken( @JsonProperty("id") int id, @@ -126,25 +182,47 @@ public TextGenerationPrefillToken( this.logProb = logProb; } + /** + * Gets the token ID. + * @return The token ID. + */ public int getId() { return id; } + /** + * Gets the token text. + * @return The token text. + */ @Nullable public String getText() { return text; } + /** + * Gets the log probability of the token. + * @return The log probability of the token. + */ public double getLogProb() { return logProb; } } + /** + * Represents a generated token deserialized from a JSON response. + */ public static class TextGenerationToken extends TextGenerationPrefillToken { @JsonProperty("special") private final boolean special; + /** + * Constructor used by Jackson to deserialize a generated token. + * @param special Whether the token is special. + * @param id The token ID. + * @param text The token text. + * @param logProb The log probability of the token. + */ @JsonCreator public TextGenerationToken( @JsonProperty("special") boolean special, @@ -155,6 +233,10 @@ public TextGenerationToken( this.special = special; } + /** + * Gets whether the token is special. + * @return Whether the token is special. + */ public boolean isSpecial() { return special; } diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/TextGenerationRequest.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/TextGenerationRequest.java index fd0b60778..0e52b5681 100644 --- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/TextGenerationRequest.java +++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/TextGenerationRequest.java @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.aiservices.huggingface.models; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; @@ -11,6 +10,9 @@ import java.util.Arrays; import java.util.List; +/** + * Represents a request to generate text using the Hugging Face API. + */ @JsonInclude(Include.NON_NULL) public class TextGenerationRequest { @@ -42,6 +44,13 @@ public class TextGenerationRequest { @JsonProperty("options") private final HuggingFaceTextOptions options; + /** + * Create a new instance of TextGenerationRequest. + * @param inputs The input string to generate text for. + * @param stream Enable streaming. + * @param parameters Parameters used by the model for generation. + * @param options Options used by the model for generation. + */ public TextGenerationRequest( @Nullable String inputs, boolean stream, @@ -53,6 +62,12 @@ public TextGenerationRequest( this.options = options; } + /** + * Create a new instance of TextGenerationRequest. + * @param prompt The prompt to generate text for. + * @param executionSettings The settings for executing the prompt. + * @return A new instance of TextGenerationRequest. + */ public static TextGenerationRequest fromPromptAndExecutionSettings(String prompt, HuggingFacePromptExecutionSettings executionSettings) { return new TextGenerationRequest( @@ -72,6 +87,9 @@ public static TextGenerationRequest fromPromptAndExecutionSettings(String prompt new HuggingFaceTextOptions()); } + /** + * Parameters used by the model for generation. + */ public static class HuggingFaceTextParameters { /// @@ -156,6 +174,19 @@ public static class HuggingFaceTextParameters { @JsonProperty("details") private final Boolean details; + /** + * Creator method for jackson deserialization. + * @param topK The number of top tokens considered within the sample operation to create new text. + * @param topP The tokens that are within the sample operation of text generation. + * @param temperature The temperature of the sampling operation. + * @param repetitionPenalty The repetition penalty. + * @param maxNewTokens The amount of new tokens to be generated. + * @param maxTime The amount of time in seconds that the query should take maximum. + * @param returnFullText A value indicating whether the return results will contain the original query. + * @param numReturnSequences The number of propositions to be returned. + * @param doSample A value indicating whether to use sampling. + * @param details A value indicating whether to include the details of the generation. + */ public HuggingFaceTextParameters( @JsonProperty("top_k") @Nullable Integer topK, @JsonProperty("top_p") @Nullable Double topP, @@ -179,56 +210,99 @@ public HuggingFaceTextParameters( this.details = details; } + /** + * Gets the number of top tokens considered within the sample operation to create new text. + * @return The number of top tokens considered within the sample operation to create new text. + */ @Nullable public Integer getTopK() { return topK; } + /** + * Gets the tokens that are within the sample operation of text generation. + * @return The tokens that are within the sample operation of text generation. + */ @Nullable public Double getTopP() { return topP; } + /** + * Gets the temperature of the sampling operation. + * @return The temperature of the sampling operation. + */ @Nullable public Double getTemperature() { return temperature; } + /** + * Gets the repetition penalty. + * @return The repetition penalty. + */ @Nullable public Double getRepetitionPenalty() { return repetitionPenalty; } + /** + * Gets the amount of new tokens to be generated. + * @return The amount of new tokens to be generated. + */ @Nullable public Integer getMaxNewTokens() { return maxNewTokens; } + /** + * Gets the amount of time in seconds that the query should take maximum. + * @return The amount of time in seconds that the query should take maximum. + */ @Nullable public Double getMaxTime() { return maxTime; } + /** + * Gets a value indicating whether the return results will contain the original query. + * @return A value indicating whether the return results will contain the original query. + */ public boolean isReturnFullText() { return returnFullText; } + /** + * Gets the number of propositions to be returned. + * @return The number of propositions to be returned. + */ @Nullable public Integer getNumReturnSequences() { return numReturnSequences; } + /** + * Gets a value indicating whether to use sampling. + * @return A value indicating whether to use sampling. + */ @Nullable public Boolean getDoSample() { return doSample; } + /** + * Gets a value indicating whether to include the details of the generation. + * @return A value indicating whether to include the details of the generation. + */ @Nullable public Boolean getDetails() { return details; } } + /** + * Options used by the model for generation. + */ @SuppressFBWarnings("SS_SHOULD_BE_STATIC") public static class HuggingFaceTextOptions { @@ -249,10 +323,18 @@ public static class HuggingFaceTextOptions { @JsonProperty("wait_for_model") private final boolean waitForModel = false; + /** + * Gets a value indicating whether to use the cache layer on the inference API. + * @return A value indicating whether to use the cache layer on the inference API. + */ public boolean isUseCache() { return useCache; } + /** + * Gets a value indicating whether to wait for the model if it is not ready. + * @return A value indicating whether to wait for the model if it is not ready. + */ public boolean isWaitForModel() { return waitForModel; } diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFacePromptExecutionSettings.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFacePromptExecutionSettings.java index ed3a4df5e..ac4f4c093 100644 --- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFacePromptExecutionSettings.java +++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFacePromptExecutionSettings.java @@ -2,11 +2,15 @@ package com.microsoft.semantickernel.aiservices.huggingface.services; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.orchestration.responseformat.ResponseFormat; import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; +/** + * Represents the settings for executing a prompt with the Hugging Face API. + */ public class HuggingFacePromptExecutionSettings extends PromptExecutionSettings { @Nullable @@ -24,6 +28,11 @@ public class HuggingFacePromptExecutionSettings extends PromptExecutionSettings @Nullable private final Long seed; + /** + * Create a new instance of HuggingFacePromptExecutionSettings. + * + * @param copy The PromptExecutionSettings to copy. + */ public HuggingFacePromptExecutionSettings(PromptExecutionSettings copy) { super( copy.getServiceId(), @@ -38,7 +47,8 @@ public HuggingFacePromptExecutionSettings(PromptExecutionSettings copy) { copy.getUser(), copy.getStopSequences(), copy.getTokenSelectionBiases(), - copy.getResponseFormat() == null ? null : copy.getResponseFormat().toString()); + copy.getResponseFormat() == null ? null : copy.getResponseFormat(), + copy.getMaxCompletionTokens() == null ? null : copy.getMaxCompletionTokens().toString()); this.topK = null; this.repetitionPenalty = null; this.maxTime = null; @@ -64,6 +74,13 @@ public HuggingFacePromptExecutionSettings(PromptExecutionSettings copy) { * @param stopSequences The stop sequences to use for prompt execution. * @param tokenSelectionBiases The token selection biases to use for prompt execution. * @param responseFormat The response format to use for prompt execution + * @param topK The topK setting for prompt execution. + * @param repetitionPenalty The repetition penalty setting for prompt execution. + * @param maxTime The max time setting for prompt execution. + * @param details The details setting for prompt execution. + * @param logProbs The logprobs setting for prompt execution. + * @param topLogProbs The top log probs setting for prompt execution. + * @param seed The seed setting for prompt execution */ public HuggingFacePromptExecutionSettings( String serviceId, @@ -78,17 +95,19 @@ public HuggingFacePromptExecutionSettings( String user, @Nullable List stopSequences, @Nullable Map tokenSelectionBiases, - @Nullable String responseFormat, + @Nullable ResponseFormat responseFormat, @Nullable Integer topK, @Nullable Double repetitionPenalty, @Nullable Double maxTime, @Nullable Boolean details, @Nullable Boolean logProbs, @Nullable Integer topLogProbs, - @Nullable Long seed) { + @Nullable Long seed, + @Nullable Boolean maxCompletionTokensEnable) { super( serviceId, modelId, temperature, topP, presencePenalty, frequencyPenalty, maxTokens, - resultsPerPrompt, bestOf, user, stopSequences, tokenSelectionBiases, responseFormat); + resultsPerPrompt, bestOf, user, stopSequences, tokenSelectionBiases, responseFormat, + Boolean.toString(Boolean.TRUE.equals(maxCompletionTokensEnable))); this.topK = topK; this.repetitionPenalty = repetitionPenalty; @@ -99,6 +118,14 @@ public HuggingFacePromptExecutionSettings( this.seed = seed; } + /** + * Create a new instance of PromptExecutionSettings from a PromptExecutionSettings. + * This method handles the whether the PromptExecutionSettings is already a + * HuggingFacePromptExecutionSettings or a new instance needs to be created + * from the provided PromptExecutionSettings. + * @param promptExecutionSettings The PromptExecutionSettings to copy. + * @return The PromptExecutionSettings mapped to a HuggingFacePromptExecutionSettings. + */ public static HuggingFacePromptExecutionSettings fromExecutionSettings( PromptExecutionSettings promptExecutionSettings) { if (promptExecutionSettings instanceof HuggingFacePromptExecutionSettings) { @@ -119,7 +146,7 @@ public static HuggingFacePromptExecutionSettings fromExecutionSettings( promptExecutionSettings.getStopSequences(), promptExecutionSettings.getTokenSelectionBiases(), promptExecutionSettings.getResponseFormat() != null - ? promptExecutionSettings.getResponseFormat().toString() + ? promptExecutionSettings.getResponseFormat() : null, null, null, @@ -127,39 +154,68 @@ public static HuggingFacePromptExecutionSettings fromExecutionSettings( null, null, null, + null, null); } + /** + * Gets the topK setting for prompt execution. + * @return The topK setting for prompt execution + */ @Nullable public Integer getTopK() { return topK; } + /** + * Gets the repetition penalty setting for prompt execution. + * @return The repetition penalty setting for prompt execution + */ @Nullable public Double getRepetitionPenalty() { return repetitionPenalty; } + /** + * Gets the max time setting for prompt execution. + * @return The max time setting for prompt execution + */ @Nullable public Double getMaxTime() { return maxTime; } + /** + * Gets the details setting for prompt execution. + * @return The details setting for prompt execution + */ @Nullable public Boolean getDetails() { return details; } + /** + * Gets the logprobs setting for prompt execution. + * @return The logprobs setting for prompt execution + */ @Nullable public Boolean getLogprobs() { return logProbs; } + /** + * Gets the top log probs setting for prompt execution. + * @return The top log probs setting for prompt execution + */ @Nullable public Integer getTopLogProbs() { return topLogProbs; } + /** + * Gets the seed setting for prompt execution. + * @return The seed setting for prompt execution + */ @Nullable public Long getSeed() { return seed; diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFaceTextGenerationService.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFaceTextGenerationService.java index 96e05ff99..a08b42326 100644 --- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFaceTextGenerationService.java +++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFaceTextGenerationService.java @@ -9,7 +9,7 @@ import com.microsoft.semantickernel.exceptions.SKException; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; -import com.microsoft.semantickernel.services.textcompletion.StreamingTextContent; +import com.microsoft.semantickernel.services.StreamingTextContent; import com.microsoft.semantickernel.services.textcompletion.TextContent; import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; import java.util.List; @@ -19,12 +19,21 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +/** + * A service that generates text using the Hugging Face API. + */ public class HuggingFaceTextGenerationService implements TextGenerationService { private final String modelId; private final String serviceId; private final HuggingFaceClient client; + /** + * Create a new instance of HuggingFaceTextGenerationService. + * @param modelId The model ID. + * @param serviceId The service ID. + * @param client The Hugging Face client. + */ public HuggingFaceTextGenerationService( String modelId, String serviceId, @@ -34,6 +43,13 @@ public HuggingFaceTextGenerationService( this.client = client; } + /** + * Get the response to a prompt. + * @param prompt The prompt. + * @param huggingFacePromptExecutionSettings The settings for executing the prompt. + * @param kernel The semantic kernel. + * @return The response to the prompt. + */ public Mono> getTextContentsAsync( String prompt, @Nullable HuggingFacePromptExecutionSettings huggingFacePromptExecutionSettings, @@ -117,10 +133,17 @@ public String getServiceId() { return serviceId; } + /** + * Create a new builder for HuggingFaceTextGenerationService. + * @return The builder. + */ public static Builder builder() { return new Builder(); } + /** + * A builder for HuggingFaceTextGenerationService. + */ public static class Builder { @Nullable @@ -152,11 +175,20 @@ public Builder withServiceId(String serviceId) { return this; } + /** + * Sets the HuggingFaceClient for the service + * @param client The HuggingFaceClient + * @return The builder + */ public Builder withHuggingFaceClient(HuggingFaceClient client) { this.client = client; return this; } + /** + * Builds the HuggingFaceTextGenerationService + * @return The HuggingFaceTextGenerationService + */ public HuggingFaceTextGenerationService build() { if (this.modelId == null) { diff --git a/aiservices/openai/pom.xml b/aiservices/openai/pom.xml index dbb1980c6..4b46bdd4a 100644 --- a/aiservices/openai/pom.xml +++ b/aiservices/openai/pom.xml @@ -6,7 +6,7 @@ com.microsoft.semantic-kernel semantickernel-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../../pom.xml @@ -19,6 +19,30 @@ com.microsoft.semantic-kernel semantickernel-api + + com.microsoft.semantic-kernel + semantickernel-api-data + provided + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + provided + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + provided + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + com.microsoft.semantic-kernel + semantickernel-api-textembedding-services + + com.azure azure-ai-openai @@ -55,6 +79,17 @@ mockito-core test + + + com.github.victools + jsonschema-generator + true + + + com.github.victools + jsonschema-module-jackson + true + diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/OpenAiService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/OpenAiService.java index 4a7a196b4..0edee4767 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/OpenAiService.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/OpenAiService.java @@ -6,6 +6,7 @@ /** * Provides OpenAI service. + * @param the client type */ public abstract class OpenAiService implements AIService { @@ -38,10 +39,18 @@ public String getServiceId() { return serviceId; } + /** + * Gets the client. + * @return the client + */ protected Client getClient() { return client; } + /** + * Gets the deployment name. + * @return the deployment name + */ public String getDeploymentName() { return deploymentName; } diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiAudioToTextService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiAudioToTextService.java index 631f2cac8..7a126f6d8 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiAudioToTextService.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiAudioToTextService.java @@ -28,6 +28,7 @@ public class OpenAiAudioToTextService extends OpenAiService * * @param client OpenAI client. * @param modelId The model ID. + * @param deploymentName The deployment name. */ public OpenAiAudioToTextService( OpenAIAsyncClient client, diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiTextToAudioService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiTextToAudioService.java index c698fab39..dc748af7d 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiTextToAudioService.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiTextToAudioService.java @@ -27,6 +27,7 @@ public class OpenAiTextToAudioService extends OpenAiService * * @param client OpenAI client. * @param modelId The model ID. + * @param deploymentName The deployment name. */ public OpenAiTextToAudioService( OpenAIAsyncClient client, @@ -79,7 +80,6 @@ public static Builder builder() { /** * Represents a builder for OpenAi text to audio service. */ - public static class Builder extends TextToAudioService.Builder { /** diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/BinaryDataUtils.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/BinaryDataUtils.java new file mode 100644 index 000000000..9a3189a7e --- /dev/null +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/BinaryDataUtils.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.azure.core.util.BinaryData; +import javax.annotation.Nullable; + +public class BinaryDataUtils { + + @Nullable + public static String toString(@Nullable BinaryData b) { + if (b == null) { + return null; + } + return b.toString(); + } +} diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java index 84a8287ee..8f0ebaf20 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java @@ -6,11 +6,15 @@ import com.azure.ai.openai.models.ChatCompletions; import com.azure.ai.openai.models.ChatCompletionsFunctionToolCall; import com.azure.ai.openai.models.ChatCompletionsFunctionToolDefinition; +import com.azure.ai.openai.models.ChatCompletionsFunctionToolDefinitionFunction; import com.azure.ai.openai.models.ChatCompletionsJsonResponseFormat; +import com.azure.ai.openai.models.ChatCompletionsNamedToolSelection; import com.azure.ai.openai.models.ChatCompletionsOptions; import com.azure.ai.openai.models.ChatCompletionsTextResponseFormat; import com.azure.ai.openai.models.ChatCompletionsToolCall; import com.azure.ai.openai.models.ChatCompletionsToolDefinition; +import com.azure.ai.openai.models.ChatCompletionsToolSelection; +import com.azure.ai.openai.models.ChatCompletionsToolSelectionPreset; import com.azure.ai.openai.models.ChatMessageImageContentItem; import com.azure.ai.openai.models.ChatMessageImageDetailLevel; import com.azure.ai.openai.models.ChatMessageImageUrl; @@ -23,26 +27,35 @@ import com.azure.ai.openai.models.ChatResponseMessage; import com.azure.ai.openai.models.CompletionsUsage; import com.azure.ai.openai.models.FunctionCall; -import com.azure.core.util.BinaryData; +import com.azure.ai.openai.models.FunctionDefinition; +import com.azure.json.JsonOptions; +import com.azure.json.implementation.DefaultJsonReader; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ContainerNode; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.aiservices.openai.OpenAiService; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.responseformat.ChatCompletionsJsonSchemaResponseFormat; import com.microsoft.semantickernel.aiservices.openai.implementation.OpenAIRequestSettings; +import com.microsoft.semantickernel.contents.FunctionCallContent; import com.microsoft.semantickernel.contextvariables.ContextVariable; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.exceptions.AIException; import com.microsoft.semantickernel.exceptions.AIException.ErrorCodes; import com.microsoft.semantickernel.exceptions.SKCheckedException; import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.functionchoice.AutoFunctionChoiceBehavior; +import com.microsoft.semantickernel.functionchoice.FunctionChoiceBehavior; +import com.microsoft.semantickernel.functionchoice.NoneFunctionChoiceBehavior; +import com.microsoft.semantickernel.functionchoice.RequiredFunctionChoiceBehavior; import com.microsoft.semantickernel.hooks.KernelHookEvent; import com.microsoft.semantickernel.hooks.KernelHooks; import com.microsoft.semantickernel.hooks.PostChatCompletionEvent; import com.microsoft.semantickernel.hooks.PreChatCompletionEvent; import com.microsoft.semantickernel.hooks.PreToolCallEvent; import com.microsoft.semantickernel.implementation.CollectionUtil; +import com.microsoft.semantickernel.implementation.telemetry.ChatCompletionSpan; import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; @@ -50,20 +63,24 @@ import com.microsoft.semantickernel.orchestration.InvocationReturnMode; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.orchestration.ToolCallBehavior; +import com.microsoft.semantickernel.orchestration.responseformat.JsonResponseSchema; +import com.microsoft.semantickernel.orchestration.responseformat.JsonSchemaResponseFormat; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent; import com.microsoft.semantickernel.services.chatcompletion.message.ChatMessageContentType; import com.microsoft.semantickernel.services.chatcompletion.message.ChatMessageImageContent; import com.microsoft.semantickernel.services.openai.OpenAiServiceBuilder; -import io.opentelemetry.api.trace.Span; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -132,7 +149,7 @@ public Mono>> getChatMessageContentsAsync( if (invocationContext != null && invocationContext - .returnMode() == InvocationReturnMode.LAST_MESSAGE_ONLY) { + .returnMode() == InvocationReturnMode.LAST_MESSAGE_ONLY) { chatHistoryResult = new ChatHistory( Collections.singletonList( CollectionUtil.getLastOrNull(chatHistoryResult.getMessages()))); @@ -166,7 +183,7 @@ public Mono>> getChatMessageContentsAsync( if (invocationContext != null && invocationContext - .returnMode() == InvocationReturnMode.LAST_MESSAGE_ONLY) { + .returnMode() == InvocationReturnMode.LAST_MESSAGE_ONLY) { result = new ChatHistory( Collections.singletonList( CollectionUtil.getLastOrNull(result.getMessages()))); @@ -179,12 +196,114 @@ public Mono>> getChatMessageContentsAsync( }); } + @Override + public Flux> getStreamingChatMessageContentsAsync( + ChatHistory chatHistory, + @Nullable Kernel kernel, + @Nullable InvocationContext invocationContext) { + if (invocationContext != null && + invocationContext.getToolCallBehavior() != null && + invocationContext.getToolCallBehavior().isAutoInvokeAllowed()) { + throw new SKException( + "ToolCallBehavior auto-invoke is not supported for streaming chat message contents"); + } + + if (invocationContext != null && + invocationContext.getFunctionChoiceBehavior() != null && + invocationContext.getFunctionChoiceBehavior() instanceof AutoFunctionChoiceBehavior && + ((AutoFunctionChoiceBehavior) invocationContext.getFunctionChoiceBehavior()) + .isAutoInvoke()) { + throw new SKException( + "FunctionChoiceBehavior auto-invoke is not supported for streaming chat message contents"); + } + + if (invocationContext != null + && invocationContext.returnMode() != InvocationReturnMode.NEW_MESSAGES_ONLY) { + throw new SKException( + "Streaming chat message contents only supports NEW_MESSAGES_ONLY return mode"); + } + + List chatRequestMessages = getChatRequestMessages(chatHistory); + + ChatMessages messages = new ChatMessages(chatRequestMessages); + + List functions = new ArrayList<>(); + if (kernel != null) { + kernel.getPlugins() + .forEach(plugin -> plugin.getFunctions().forEach((name, function) -> functions + .add(OpenAIFunction.build(function.getMetadata(), plugin.getName())))); + } + + OpenAIToolCallConfig toolCallConfig = getToolCallConfig( + invocationContext, + functions, + messages.allMessages, + 0); + + ChatCompletionsOptions options = executeHook( + invocationContext, + kernel, + new PreChatCompletionEvent( + getCompletionsOptions( + this, + messages.allMessages, + invocationContext, + toolCallConfig))) + .getOptions(); + + return getClient() + .getChatCompletionsStreamWithResponse( + getDeploymentName(), + options, + OpenAIRequestSettings.getRequestOptions()) + .flatMap(completionsResult -> { + if (completionsResult.getStatusCode() >= 400) { + //SemanticKernelTelemetry.endSpanWithError(span); + return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR, + "Request failed: " + completionsResult.getStatusCode())); + } + //SemanticKernelTelemetry.endSpanWithUsage(span, completionsResult.getValue().getUsage()); + + return Mono.just(completionsResult.getValue()); + }) + .flatMap(completions -> { + return Flux.fromIterable(completions.getChoices()) + .map(message -> { + AuthorRole role = message.getDelta().getRole() == null + ? AuthorRole.ASSISTANT + : AuthorRole.valueOf(message.getDelta().getRole().toString() + .toUpperCase(Locale.ROOT)); + + return new OpenAIStreamingChatMessageContent<>( + completions.getId(), + role, + message.getDelta().getContent(), + getModelId(), + null, + null, + null, + Arrays.asList()); + }); + }); + } + + @Override + public Flux> getStreamingChatMessageContentsAsync( + String prompt, + @Nullable Kernel kernel, + @Nullable InvocationContext invocationContext) { + return getStreamingChatMessageContentsAsync( + new ChatHistory().addUserMessage(prompt), + kernel, + invocationContext); + } + // Holds messages temporarily as we build up our result private static class ChatMessages { private final List newMessages; private final List allMessages; - private final List newChatMessageContent; + private final List> newChatMessageContent; public ChatMessages(List allMessages) { this.allMessages = Collections.unmodifiableList(allMessages); @@ -195,7 +314,7 @@ public ChatMessages(List allMessages) { private ChatMessages( List allMessages, List newMessages, - List newChatMessageContent) { + List> newChatMessageContent) { this.allMessages = Collections.unmodifiableList(allMessages); this.newMessages = Collections.unmodifiableList(newMessages); this.newChatMessageContent = Collections.unmodifiableList(newChatMessageContent); @@ -219,8 +338,8 @@ public ChatMessages add(ChatRequestMessage requestMessage) { } @CheckReturnValue - public ChatMessages addChatMessage(List chatMessageContent) { - ArrayList tmpChatMessageContent = new ArrayList<>( + public ChatMessages addChatMessage(List> chatMessageContent) { + ArrayList> tmpChatMessageContent = new ArrayList<>( newChatMessageContent); tmpChatMessageContent.addAll(chatMessageContent); @@ -291,16 +410,12 @@ private Mono internalChatMessageContentsAsync( .add(OpenAIFunction.build(function.getMetadata(), plugin.getName())))); } - // Create copy to avoid reactor exceptions when updating request messages internally return internalChatMessageContentsAsync( messages, kernel, functions, invocationContext, - Math.min(MAXIMUM_INFLIGHT_AUTO_INVOKES, - invocationContext != null && invocationContext.getToolCallBehavior() != null - ? invocationContext.getToolCallBehavior().getMaximumAutoInvokeAttempts() - : 0)); + 0); } private Mono internalChatMessageContentsAsync( @@ -308,7 +423,13 @@ private Mono internalChatMessageContentsAsync( @Nullable Kernel kernel, List functions, @Nullable InvocationContext invocationContext, - int autoInvokeAttempts) { + int requestIndex) { + + OpenAIToolCallConfig toolCallConfig = getToolCallConfig( + invocationContext, + functions, + messages.allMessages, + requestIndex); ChatCompletionsOptions options = executeHook( invocationContext, @@ -317,32 +438,37 @@ private Mono internalChatMessageContentsAsync( getCompletionsOptions( this, messages.allMessages, - functions, - invocationContext))) + invocationContext, + toolCallConfig))) .getOptions(); - Span span = SemanticKernelTelemetry.startChatCompletionSpan( - getModelId(), - SemanticKernelTelemetry.OPEN_AI_PROVIDER, - options.getMaxTokens(), - options.getTemperature(), - options.getTopP()); - return getClient() - .getChatCompletionsWithResponse(getDeploymentName(), options, - OpenAIRequestSettings.getRequestOptions()) - .flatMap(completionsResult -> { - if (completionsResult.getStatusCode() >= 400) { - SemanticKernelTelemetry.endSpanWithError(span); - return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR, - "Request failed: " + completionsResult.getStatusCode())); - } - SemanticKernelTelemetry.endSpanWithUsage(span, - completionsResult.getValue().getUsage()); + return Mono.deferContextual(contextView -> { + ChatCompletionSpan span = ChatCompletionSpan.startChatCompletionSpan( + SemanticKernelTelemetry.getTelemetry(invocationContext), + contextView, + getModelId(), + SemanticKernelTelemetry.OPEN_AI_PROVIDER, + options.getMaxTokens(), + options.getTemperature(), + options.getTopP()); + + return getClient() + .getChatCompletionsWithResponse(getDeploymentName(), options, + OpenAIRequestSettings.getRequestOptions()) + .contextWrite(span.getReactorContextModifier()) + .flatMap(completionsResult -> { + if (completionsResult.getStatusCode() >= 400) { + return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR, + "Request failed: " + completionsResult.getStatusCode())); + } - return Mono.just(completionsResult.getValue()); + return Mono.just(completionsResult.getValue()); + }) + .doOnError(span::endSpanWithError) + .doOnSuccess(span::endSpanWithUsage) + .doOnTerminate(span::close); }) .flatMap(completions -> { - List responseMessages = completions .getChoices() .stream() @@ -354,22 +480,22 @@ private Mono internalChatMessageContentsAsync( executeHook(invocationContext, kernel, new PostChatCompletionEvent(completions)); // Just return the result: - // If we don't want to attempt to invoke any functions + // If auto-invoking is not enabled // Or if we are auto-invoking, but we somehow end up with other than 1 choice even though only 1 was requested - if (autoInvokeAttempts == 0 || responseMessages.size() != 1) { - return getChatMessageContentsAsync(completions) - .flatMap(m -> { - return Mono.just(messages.addChatMessage(m)); - }); + if (toolCallConfig == null || !toolCallConfig.isAutoInvoke() + || responseMessages.size() != 1) { + List> chatMessageContents = getChatMessageContentsAsync( + completions); + return Mono.just(messages.addChatMessage(chatMessageContents)); } + // Or if there are no tool calls to be done ChatResponseMessage response = responseMessages.get(0); List toolCalls = response.getToolCalls(); if (toolCalls == null || toolCalls.isEmpty()) { - return getChatMessageContentsAsync(completions) - .flatMap(m -> { - return Mono.just(messages.addChatMessage(m)); - }); + List> chatMessageContents = getChatMessageContentsAsync( + completions); + return Mono.just(messages.addChatMessage(chatMessageContents)); } ChatRequestAssistantMessage requestMessage = new ChatRequestAssistantMessage( @@ -395,14 +521,14 @@ private Mono internalChatMessageContentsAsync( .flatMap(it -> it) .flatMap(msgs -> { return internalChatMessageContentsAsync(msgs, kernel, functions, - invocationContext, autoInvokeAttempts - 1); + invocationContext, requestIndex + 1); }) .onErrorResume(e -> { LOGGER.warn("Tool invocation attempt failed: ", e); // If FunctionInvocationError occurred and there are still attempts left, retry, else exit - if (autoInvokeAttempts > 0) { + if (requestIndex < MAXIMUM_INFLIGHT_AUTO_INVOKES) { ChatMessages currentMessages = messages; if (e instanceof FunctionInvocationError) { currentMessages.assertCommonHistory( @@ -416,7 +542,7 @@ private Mono internalChatMessageContentsAsync( kernel, functions, invocationContext, - autoInvokeAttempts - 1); + requestIndex + 1); } else { return Mono.error(e); } @@ -508,8 +634,8 @@ private Mono> invokeFunctionTool( ContextVariableTypes contextVariableTypes) { try { - OpenAIFunctionToolCall openAIFunctionToolCall = extractOpenAIFunctionToolCall(toolCall); - String pluginName = openAIFunctionToolCall.getPluginName(); + FunctionCallContent functionCallContent = extractFunctionCallContent(toolCall); + String pluginName = functionCallContent.getPluginName(); if (pluginName == null || pluginName.isEmpty()) { return Mono.error( new SKException("Plugin name is required for function tool call")); @@ -517,21 +643,23 @@ private Mono> invokeFunctionTool( KernelFunction function = kernel.getFunction( pluginName, - openAIFunctionToolCall.getFunctionName()); + functionCallContent.getFunctionName()); PreToolCallEvent hookResult = executeHook(invocationContext, kernel, new PreToolCallEvent( - openAIFunctionToolCall.getFunctionName(), - openAIFunctionToolCall.getArguments(), + functionCallContent.getFunctionName(), + functionCallContent.getArguments(), function, contextVariableTypes)); function = hookResult.getFunction(); - KernelFunctionArguments arguments = hookResult.getArguments(); + KernelArguments arguments = hookResult.getArguments(); return function .invokeAsync(kernel) .withArguments(arguments) + .withTypes(invocationContext.getContextVariableTypes()) + .withTypes(contextVariableTypes) .withResultType(contextVariableTypes.getVariableTypeForClass(String.class)); } catch (JsonProcessingException e) { return Mono.error(new SKException("Failed to parse tool arguments", e)); @@ -559,7 +687,7 @@ private static T executeHook( } @SuppressWarnings("StringSplitter") - private OpenAIFunctionToolCall extractOpenAIFunctionToolCall( + private FunctionCallContent extractFunctionCallContent( ChatCompletionsFunctionToolCall toolCall) throws JsonProcessingException { @@ -569,7 +697,7 @@ private OpenAIFunctionToolCall extractOpenAIFunctionToolCall( String pluginName = parts.length > 1 ? parts[0] : ""; String fnName = parts.length > 1 ? parts[1] : parts[0]; - KernelFunctionArguments arguments = KernelFunctionArguments.builder().build(); + KernelArguments arguments = KernelArguments.builder().build(); ObjectMapper mapper = new ObjectMapper(); JsonNode jsonToolCallArguments = mapper.readTree(toolCall.getFunction().getArguments()); @@ -585,14 +713,14 @@ private OpenAIFunctionToolCall extractOpenAIFunctionToolCall( } }); - return new OpenAIFunctionToolCall( - toolCall.getId(), - pluginName, + return new FunctionCallContent( fnName, + pluginName, + toolCall.getId(), arguments); } - private Mono> getChatMessageContentsAsync( + private List> getChatMessageContentsAsync( ChatCompletions completions) { FunctionResultMetadata completionMetadata = FunctionResultMetadata.build( completions.getId(), @@ -606,22 +734,27 @@ private Mono> getChatMessageContentsAsync( .filter(Objects::nonNull) .collect(Collectors.toList()); - return Flux.fromIterable(responseMessages) - .flatMap(response -> { + List> chatMessageContent = responseMessages + .stream() + .map(response -> { try { - return Mono.just(new OpenAIChatMessageContent( + return new OpenAIChatMessageContent<>( AuthorRole.ASSISTANT, response.getContent(), this.getModelId(), null, null, completionMetadata, - formOpenAiToolCalls(response))); - } catch (Exception e) { - return Mono.error(e); + formFunctionCallContents(response)); + } catch (SKCheckedException e) { + LOGGER.warn("Failed to form chat message content", e); + return null; } }) - .collectList(); + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + return chatMessageContent; } private List> toOpenAIChatMessageContent( @@ -633,7 +766,8 @@ private List> toOpenAIChatMessageContent( if (message instanceof ChatRequestUserMessage) { return new OpenAIChatMessageContent<>( AuthorRole.USER, - ((ChatRequestUserMessage) message).getContent().toString(), + BinaryDataUtils + .toString(((ChatRequestUserMessage) message).getContent()), null, null, null, @@ -642,7 +776,8 @@ private List> toOpenAIChatMessageContent( } else if (message instanceof ChatRequestSystemMessage) { return new OpenAIChatMessageContent<>( AuthorRole.SYSTEM, - ((ChatRequestSystemMessage) message).getContent(), + BinaryDataUtils + .toString(((ChatRequestSystemMessage) message).getContent()), null, null, null, @@ -650,11 +785,12 @@ private List> toOpenAIChatMessageContent( null); } else if (message instanceof ChatRequestAssistantMessage) { try { - List calls = getToolCalls( + List calls = getFunctionCallContents( ((ChatRequestAssistantMessage) message).getToolCalls()); return new OpenAIChatMessageContent<>( AuthorRole.ASSISTANT, - ((ChatRequestAssistantMessage) message).getContent(), + BinaryDataUtils + .toString(((ChatRequestAssistantMessage) message).getContent()), null, null, null, @@ -666,7 +802,8 @@ private List> toOpenAIChatMessageContent( } else if (message instanceof ChatRequestToolMessage) { return new OpenAIChatMessageContent<>( AuthorRole.TOOL, - ((ChatRequestToolMessage) message).getContent(), + BinaryDataUtils + .toString(((ChatRequestToolMessage) message).getContent()), null, null, null, @@ -687,7 +824,7 @@ private List> toOpenAIChatMessageContent( } @Nullable - private List getToolCalls( + private List getFunctionCallContents( @Nullable List toolCalls) throws SKCheckedException { if (toolCalls == null || toolCalls.isEmpty()) { return null; @@ -699,7 +836,7 @@ private List getToolCalls( .map(call -> { if (call instanceof ChatCompletionsFunctionToolCall) { try { - return extractOpenAIFunctionToolCall( + return extractFunctionCallContent( (ChatCompletionsFunctionToolCall) call); } catch (JsonProcessingException e) { throw SKException.build("Failed to parse tool arguments", e); @@ -716,7 +853,7 @@ private List getToolCalls( } @Nullable - private List formOpenAiToolCalls( + private List formFunctionCallContents( ChatResponseMessage response) throws SKCheckedException { if (response.getToolCalls() == null || response.getToolCalls().isEmpty()) { return null; @@ -728,7 +865,7 @@ private List formOpenAiToolCalls( .map(call -> { if (call instanceof ChatCompletionsFunctionToolCall) { try { - return extractOpenAIFunctionToolCall( + return extractFunctionCallContent( (ChatCompletionsFunctionToolCall) call); } catch (JsonProcessingException e) { throw SKException.build("Failed to parse tool arguments", e); @@ -747,8 +884,8 @@ private List formOpenAiToolCalls( private static ChatCompletionsOptions getCompletionsOptions( ChatCompletionService chatCompletionService, List chatRequestMessages, - @Nullable List functions, - @Nullable InvocationContext invocationContext) { + @Nullable InvocationContext invocationContext, + @Nullable OpenAIToolCallConfig toolCallConfig) { chatRequestMessages = chatRequestMessages .stream() @@ -758,12 +895,13 @@ private static ChatCompletionsOptions getCompletionsOptions( ChatCompletionsOptions options = new ChatCompletionsOptions(chatRequestMessages) .setModel(chatCompletionService.getModelId()); - if (invocationContext != null && invocationContext.getToolCallBehavior() != null) { - configureToolCallBehaviorOptions( - options, - invocationContext.getToolCallBehavior(), - functions, - chatRequestMessages); + if (toolCallConfig != null) { + options.setTools(toolCallConfig.getTools()); + options.setToolChoice(toolCallConfig.getToolChoice()); + + if (toolCallConfig.getOptions() != null) { + options.setParallelToolCalls(toolCallConfig.getOptions().isParallelCallsAllowed()); + } } PromptExecutionSettings promptExecutionSettings = invocationContext != null @@ -782,7 +920,8 @@ private static ChatCompletionsOptions getCompletionsOptions( } Map logit = null; - if (promptExecutionSettings.getTokenSelectionBiases() != null) { + if (promptExecutionSettings.getTokenSelectionBiases() != null + && !promptExecutionSettings.getTokenSelectionBiases().isEmpty()) { logit = promptExecutionSettings .getTokenSelectionBiases() .entrySet() @@ -799,23 +938,32 @@ private static ChatCompletionsOptions getCompletionsOptions( .setFrequencyPenalty(promptExecutionSettings.getFrequencyPenalty()) .setPresencePenalty(promptExecutionSettings.getPresencePenalty()) .setMaxTokens(promptExecutionSettings.getMaxTokens()) + .setMaxCompletionTokens(promptExecutionSettings.getMaxCompletionTokens()) .setN(promptExecutionSettings.getResultsPerPrompt()) // Azure OpenAI WithData API does not allow to send empty array of stop sequences // Gives back "Validation error at #/stop/str: Input should be a valid string\nValidation error at #/stop/list[str]: List should have at least 1 item after validation, not 0" .setStop(promptExecutionSettings.getStopSequences() == null || promptExecutionSettings.getStopSequences().isEmpty() ? null - : promptExecutionSettings.getStopSequences()) + : promptExecutionSettings.getStopSequences()) .setUser(promptExecutionSettings.getUser()) .setLogitBias(logit); if (promptExecutionSettings.getResponseFormat() != null) { - switch (promptExecutionSettings.getResponseFormat()) { + switch (promptExecutionSettings.getResponseFormat().getType()) { + case JSON_SCHEMA: + JsonResponseSchema schema = ((JsonSchemaResponseFormat) promptExecutionSettings + .getResponseFormat()) + .getJsonSchema(); + + options.setResponseFormat(new ChatCompletionsJsonSchemaResponseFormat(schema)); + break; case JSON_OBJECT: options.setResponseFormat(new ChatCompletionsJsonResponseFormat()); break; case TEXT: options.setResponseFormat(new ChatCompletionsTextResponseFormat()); break; + default: throw new SKException( "Unknown response format: " + promptExecutionSettings.getResponseFormat()); @@ -825,76 +973,184 @@ private static ChatCompletionsOptions getCompletionsOptions( return options; } - private static void configureToolCallBehaviorOptions( - ChatCompletionsOptions options, + @Nullable + private static OpenAIToolCallConfig getToolCallConfig( + @Nullable InvocationContext invocationContext, + @Nullable List functions, + List chatRequestMessages, + int requestIndex) { + + if (invocationContext == null || functions == null || functions.isEmpty()) { + return null; + } + + if (invocationContext.getFunctionChoiceBehavior() == null + && invocationContext.getToolCallBehavior() == null) { + return null; + } + + if (invocationContext.getFunctionChoiceBehavior() != null) { + return getFunctionChoiceBehaviorConfig( + invocationContext.getFunctionChoiceBehavior(), + functions, + requestIndex); + } else { + return getToolCallBehaviorConfig( + invocationContext.getToolCallBehavior(), + functions, + chatRequestMessages, + requestIndex); + } + } + + @Nullable + private static OpenAIToolCallConfig getFunctionChoiceBehaviorConfig( + @Nullable FunctionChoiceBehavior functionChoiceBehavior, + @Nullable List functions, + int requestIndex) { + if (functionChoiceBehavior == null) { + return null; + } + + if (functions == null || functions.isEmpty()) { + return null; + } + + ChatCompletionsToolSelection toolChoice; + boolean autoInvoke; + + if (functionChoiceBehavior instanceof RequiredFunctionChoiceBehavior) { + // After first request a required function must have been called already + if (requestIndex >= 1) { + return null; + } + + toolChoice = new ChatCompletionsToolSelection( + ChatCompletionsToolSelectionPreset.REQUIRED); + autoInvoke = ((RequiredFunctionChoiceBehavior) functionChoiceBehavior).isAutoInvoke(); + } else if (functionChoiceBehavior instanceof AutoFunctionChoiceBehavior) { + toolChoice = new ChatCompletionsToolSelection(ChatCompletionsToolSelectionPreset.AUTO); + autoInvoke = ((AutoFunctionChoiceBehavior) functionChoiceBehavior).isAutoInvoke() + && requestIndex < MAXIMUM_INFLIGHT_AUTO_INVOKES; + } else if (functionChoiceBehavior instanceof NoneFunctionChoiceBehavior) { + toolChoice = new ChatCompletionsToolSelection(ChatCompletionsToolSelectionPreset.NONE); + autoInvoke = false; + } else { + throw new SKException( + "Unsupported function choice behavior: " + functionChoiceBehavior); + } + + // List of functions advertised to the model + List toolDefinitions = functions.stream() + .filter(function -> functionChoiceBehavior.isFunctionAllowed(function.getPluginName(), + function.getName())) + .map(OpenAIFunction::getFunctionDefinition) + .map(it -> new ChatCompletionsFunctionToolDefinitionFunction(it.getName()) + .setDescription(it.getDescription()) + .setParameters(it.getParameters())) + .map(ChatCompletionsFunctionToolDefinition::new) + .collect(Collectors.toList()); + + return new OpenAIToolCallConfig( + toolDefinitions, + toolChoice, + autoInvoke, + functionChoiceBehavior.getOptions()); + } + + @Nullable + private static OpenAIToolCallConfig getToolCallBehaviorConfig( @Nullable ToolCallBehavior toolCallBehavior, @Nullable List functions, - List chatRequestMessages) { + List chatRequestMessages, + int requestIndex) { if (toolCallBehavior == null) { - return; + return null; } if (functions == null || functions.isEmpty()) { - return; + return null; } + List toolDefinitions; + ChatCompletionsToolSelection toolChoice; + // If a specific function is required to be called if (toolCallBehavior instanceof ToolCallBehavior.RequiredKernelFunction) { - KernelFunction toolChoice = ((ToolCallBehavior.RequiredKernelFunction) toolCallBehavior) + KernelFunction requiredFunction = ((ToolCallBehavior.RequiredKernelFunction) toolCallBehavior) .getRequiredFunction(); String toolChoiceName = String.format("%s%s%s", - toolChoice.getPluginName(), + requiredFunction.getPluginName(), OpenAIFunction.getNameSeparator(), - toolChoice.getName()); + requiredFunction.getName()); // If required tool call has already been called dont ask for it again boolean hasBeenExecuted = hasToolCallBeenExecuted(chatRequestMessages, toolChoiceName); if (hasBeenExecuted) { - return; + return null; } - List toolDefinitions = new ArrayList<>(); + FunctionDefinition function = OpenAIFunction.toFunctionDefinition( + requiredFunction.getMetadata(), + requiredFunction.getPluginName()); + toolDefinitions = new ArrayList<>(); toolDefinitions.add(new ChatCompletionsFunctionToolDefinition( - OpenAIFunction.toFunctionDefinition( - toolChoice.getMetadata(), - toolChoice.getPluginName()))); + new ChatCompletionsFunctionToolDefinitionFunction(function.getName()) + .setDescription(function.getDescription()) + .setParameters(function.getParameters()))); - options.setTools(toolDefinitions); try { String json = String.format( "{\"type\":\"function\",\"function\":{\"name\":\"%s\"}}", toolChoiceName); - options.setToolChoice(BinaryData.fromObject(new ObjectMapper().readTree(json))); + + toolChoice = new ChatCompletionsToolSelection( + ChatCompletionsNamedToolSelection.fromJson( + DefaultJsonReader.fromString( + json, + new JsonOptions()))); } catch (JsonProcessingException e) { throw SKException.build("Failed to parse tool choice", e); + } catch (IOException e) { + throw new SKException(e); } - return; } - // If a set of functions are enabled to be called - ToolCallBehavior.AllowedKernelFunctions enabledKernelFunctions = (ToolCallBehavior.AllowedKernelFunctions) toolCallBehavior; - List toolDefinitions = functions.stream() - .filter(function -> { - // check if all kernel functions are enabled - if (enabledKernelFunctions.isAllKernelFunctionsAllowed()) { - return true; - } - // otherwise, check for the specific function - return enabledKernelFunctions.isFunctionAllowed(function.getPluginName(), - function.getName()); - }) - .map(OpenAIFunction::getFunctionDefinition) - .map(ChatCompletionsFunctionToolDefinition::new) - .collect(Collectors.toList()); + else { + toolChoice = new ChatCompletionsToolSelection(ChatCompletionsToolSelectionPreset.AUTO); + + ToolCallBehavior.AllowedKernelFunctions enabledKernelFunctions = (ToolCallBehavior.AllowedKernelFunctions) toolCallBehavior; + toolDefinitions = functions.stream() + .filter(function -> { + // check if all kernel functions are enabled + if (enabledKernelFunctions.isAllKernelFunctionsAllowed()) { + return true; + } + // otherwise, check for the specific function + return enabledKernelFunctions.isFunctionAllowed(function.getPluginName(), + function.getName()); + }) + .map(OpenAIFunction::getFunctionDefinition) + .map(it -> new ChatCompletionsFunctionToolDefinitionFunction(it.getName()) + .setDescription(it.getDescription()) + .setParameters(it.getParameters())) + .map(ChatCompletionsFunctionToolDefinition::new) + .collect(Collectors.toList()); - if (toolDefinitions.isEmpty()) { - return; + if (toolDefinitions.isEmpty()) { + return null; + } } - options.setTools(toolDefinitions); - options.setToolChoice(BinaryData.fromString("auto")); + return new OpenAIToolCallConfig( + toolDefinitions, + toolChoice, + toolCallBehavior.isAutoInvokeAllowed() + && requestIndex < Math.min(MAXIMUM_INFLIGHT_AUTO_INVOKES, + toolCallBehavior.getMaximumAutoInvokeAttempts()), + null); } private static boolean hasToolCallBeenExecuted(List chatRequestMessages, @@ -931,7 +1187,7 @@ private static boolean hasToolCallBeenExecuted(List chatRequ } private static List getChatRequestMessages( - List messages) { + List> messages) { if (messages == null || messages.isEmpty()) { return new ArrayList<>(); } @@ -998,24 +1254,21 @@ private static ChatRequestAssistantMessage formAssistantMessage( // TODO: handle tools other than function calls ChatRequestAssistantMessage asstMessage = new ChatRequestAssistantMessage(content); - List toolCalls = null; - if (message instanceof OpenAIChatMessageContent) { - toolCalls = ((OpenAIChatMessageContent) message).getToolCall(); - } + List toolCalls = FunctionCallContent.getFunctionCalls(message); if (toolCalls != null) { asstMessage.setToolCalls( toolCalls.stream() .map(toolCall -> { - KernelFunctionArguments arguments = toolCall.getArguments(); + KernelArguments arguments = toolCall.getArguments(); String args = arguments != null && !arguments.isEmpty() ? arguments.entrySet().stream() - .map(entry -> String.format("\"%s\": \"%s\"", - StringEscapeUtils.escapeJson(entry.getKey()), - StringEscapeUtils.escapeJson( - entry.getValue().toPromptString()))) - .collect(Collectors.joining(",", "{", "}")) + .map(entry -> String.format("\"%s\": \"%s\"", + StringEscapeUtils.escapeJson(entry.getKey()), + StringEscapeUtils.escapeJson( + entry.getValue().toPromptString()))) + .collect(Collectors.joining(",", "{", "}")) : "{}"; String prefix = ""; diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java index f2cbf858e..ed1e28329 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java @@ -1,12 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.aiservices.openai.chatcompletion; +import com.microsoft.semantickernel.contents.FunctionCallContent; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; +import com.microsoft.semantickernel.services.KernelContent; import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; import java.nio.charset.Charset; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -16,6 +19,7 @@ */ public class OpenAIChatMessageContent extends ChatMessageContent { + @Deprecated @Nullable private final List toolCall; @@ -28,7 +32,7 @@ public class OpenAIChatMessageContent extends ChatMessageContent { * @param innerContent The inner content. * @param encoding The encoding. * @param metadata The metadata. - * @param toolCall The tool call. + * @param functionCalls The tool call. */ public OpenAIChatMessageContent( AuthorRole authorRole, @@ -36,14 +40,26 @@ public OpenAIChatMessageContent( @Nullable String modelId, @Nullable T innerContent, @Nullable Charset encoding, - @Nullable FunctionResultMetadata metadata, - @Nullable List toolCall) { - super(authorRole, content, modelId, innerContent, encoding, metadata); + @Nullable FunctionResultMetadata metadata, + @Nullable List functionCalls) { + super(authorRole, content, (List>) functionCalls, modelId, + innerContent, encoding, metadata); - if (toolCall == null) { + if (functionCalls == null) { this.toolCall = null; } else { - this.toolCall = Collections.unmodifiableList(toolCall); + // Keep OpenAIFunctionToolCall list for legacy + this.toolCall = Collections.unmodifiableList(functionCalls.stream().map(t -> { + if (t instanceof OpenAIFunctionToolCall) { + return (OpenAIFunctionToolCall) t; + } else { + return new OpenAIFunctionToolCall( + t.getId(), + t.getPluginName(), + t.getFunctionName(), + t.getArguments()); + } + }).collect(Collectors.toList())); } } @@ -51,7 +67,10 @@ public OpenAIChatMessageContent( * Gets any tool calls requested. * * @return The tool call. + * + * @deprecated Use {@link FunctionCallContent#getFunctionCalls(ChatMessageContent)} instead. */ + @Deprecated @Nullable public List getToolCall() { return toolCall; diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunction.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunction.java index 0ed048e8a..4ef550c23 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunction.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunction.java @@ -7,16 +7,20 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.orchestration.responseformat.ResponseSchemaGenerator; +import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.semanticfunctions.InputVariable; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionMetadata; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; class OpenAIFunction { @@ -149,17 +153,26 @@ public Map getProperties() { private static String getSchemaForFunctionParameter(@Nullable InputVariable parameter) { List entries = new ArrayList<>(); - entries.add("\"type\":\"string\""); + String type = "string"; + + if (parameter != null && parameter.getType() != null) { + type = getJavaTypeToOpenAiFunctionType(parameter.getType()); + } + + entries.add("\"type\":\"" + type + "\""); // Add description if present + String description = null; if (parameter != null && parameter.getDescription() != null && !parameter.getDescription() .isEmpty()) { - String description = parameter.getDescription(); + description = parameter.getDescription(); description = description.replaceAll("\\r?\\n|\\r", ""); description = description.replace("\"", "\\\""); - - description = String.format("\"description\":\"%s\"", description); - entries.add(description); + entries.add(String.format("\"description\":\"%s\"", description)); + } + // If custom type, generate schema + if ("object".equalsIgnoreCase(type)) { + return getObjectSchema(parameter.getType(), description); } // Add enum options if parameter is an enum @@ -179,4 +192,48 @@ private static String getSchemaForFunctionParameter(@Nullable InputVariable para return "{" + schema + "}"; } + + private static String getJavaTypeToOpenAiFunctionType(String javaType) { + switch (javaType.toLowerCase(Locale.ROOT)) { + case "java.lang.boolean": + case "boolean": + return "boolean"; + case "java.lang.integer": + case "integer": + case "int": + case "java.lang.long": + case "long": + case "java.lang.short": + case "short": + case "java.lang.byte": + case "byte": + return "integer"; + case "java.lang.double": + case "double": + case "java.lang.float": + case "float": + return "number"; + case "java.lang.string": + case "string": + return "string"; + case "array": + return "array"; + case "java.lang.void": + case "void": + return "null"; + default: + return "object"; + } + } + + private static String getObjectSchema(String type, String description) { + Class clazz = KernelPluginFactory.getTypeForName(type); + String schema = ResponseSchemaGenerator.jacksonGenerator().generateSchema(clazz); + + Map properties = BinaryData.fromString(schema).toObject(Map.class); + if (StringUtils.isNotBlank(description)) { + properties.put("description", description); + } + return BinaryData.fromObject(properties).toString(); + } } diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java index 18111330d..c1def3379 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java @@ -1,29 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.aiservices.openai.chatcompletion; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.contents.FunctionCallContent; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import javax.annotation.Nullable; /** * Represents a call to a function in the OpenAI tool. + * + * @deprecated Use {@link FunctionCallContent} instead. */ -public class OpenAIFunctionToolCall { - - /// Gets the ID of the tool call. - @Nullable - private final String id; - - /// Gets the name of the plugin with which this function is associated, if any. - - @Nullable - private final String pluginName; - - /// Gets the name of the function. - private final String functionName; - - /// Gets a name/value collection of the arguments to the function, if any. - @Nullable - private final KernelFunctionArguments arguments; +@Deprecated +public class OpenAIFunctionToolCall extends FunctionCallContent { /** * Creates a new instance of the {@link OpenAIFunctionToolCall} class. @@ -37,56 +25,7 @@ public OpenAIFunctionToolCall( @Nullable String id, @Nullable String pluginName, String functionName, - @Nullable KernelFunctionArguments arguments) { - this.id = id; - this.pluginName = pluginName; - this.functionName = functionName; - if (arguments == null) { - this.arguments = null; - } else { - this.arguments = arguments.copy(); - } - } - - /** - * Gets the ID of the tool call. - * - * @return The ID of the tool call. - */ - @Nullable - public String getId() { - return id; - } - - /** - * Gets the name of the plugin with which this function is associated, if any. - * - * @return The name of the plugin with which this function is associated, if any. - */ - @Nullable - public String getPluginName() { - return pluginName; - } - - /** - * Gets the name of the function. - * - * @return The name of the function. - */ - public String getFunctionName() { - return functionName; - } - - /** - * Gets a name/value collection of the arguments to the function, if any. - * - * @return A name/value collection of the arguments to the function, if any. - */ - @Nullable - public KernelFunctionArguments getArguments() { - if (arguments == null) { - return null; - } - return arguments.copy(); + @Nullable KernelArguments arguments) { + super(functionName, pluginName, id, arguments); } } diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIStreamingChatMessageContent.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIStreamingChatMessageContent.java new file mode 100644 index 000000000..c919f5c6e --- /dev/null +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIStreamingChatMessageContent.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent; +import java.nio.charset.Charset; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Represents the content of a chat message. + * + * @param The type of the inner content. + */ +public class OpenAIStreamingChatMessageContent extends OpenAIChatMessageContent implements + StreamingChatContent { + + private final String id; + + /** + * Creates a new instance of the {@link OpenAIChatMessageContent} class. + * + * @param id The id of the message. + * @param authorRole The author role that generated the content. + * @param content The content. + * @param modelId The model id. + * @param innerContent The inner content. + * @param encoding The encoding. + * @param metadata The metadata. + * @param toolCall The tool call. + */ + public OpenAIStreamingChatMessageContent( + String id, + AuthorRole authorRole, + String content, + @Nullable String modelId, + @Nullable T innerContent, + @Nullable Charset encoding, + @Nullable FunctionResultMetadata metadata, + @Nullable List toolCall) { + super( + authorRole, + content, + modelId, + innerContent, + encoding, + metadata, + toolCall); + + this.id = id; + } + + @Override + public String getId() { + return id; + } +} diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIToolCallConfig.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIToolCallConfig.java new file mode 100644 index 000000000..454ed3ce1 --- /dev/null +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIToolCallConfig.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.azure.ai.openai.models.ChatCompletionsToolDefinition; +import com.azure.ai.openai.models.ChatCompletionsToolSelection; +import com.microsoft.semantickernel.functionchoice.FunctionChoiceBehaviorOptions; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; + +public class OpenAIToolCallConfig { + private final List tools; + private final ChatCompletionsToolSelection toolChoice; + private final boolean autoInvoke; + @Nullable + private final FunctionChoiceBehaviorOptions options; + + /** + * Creates a new instance of the {@link OpenAIToolCallConfig} class. + * + * @param tools The list of tools available for the call. + * @param toolChoice The tool selection strategy. + * @param autoInvoke Indicates whether to automatically invoke the tool. + * @param options Additional options for function choice behavior. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public OpenAIToolCallConfig( + List tools, + ChatCompletionsToolSelection toolChoice, + boolean autoInvoke, + @Nullable FunctionChoiceBehaviorOptions options) { + this.tools = tools; + this.toolChoice = toolChoice; + this.autoInvoke = autoInvoke; + this.options = options; + } + + /** + * Gets the list of tools available for the call. + * + * @return The list of tools. + */ + public List getTools() { + return Collections.unmodifiableList(tools); + } + + /** + * Gets the tool selection strategy. + * + * @return The tool selection strategy. + */ + public ChatCompletionsToolSelection getToolChoice() { + return toolChoice; + } + + /** + * Indicates whether to automatically invoke the tool. + * + * @return True if auto-invocation is enabled; otherwise, false. + */ + public boolean isAutoInvoke() { + return autoInvoke; + } + + /** + * Gets additional options for function choice behavior. + * + * @return The function choice behavior options. + */ + public FunctionChoiceBehaviorOptions getOptions() { + return options; + } +} diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiXMLPromptParser.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiXMLPromptParser.java index 4088f72f5..4f5fd99cc 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiXMLPromptParser.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiXMLPromptParser.java @@ -118,19 +118,23 @@ public static ChatRequestMessage unescapeRequest(ChatRequestMessage message) { if (message instanceof ChatRequestUserMessage) { ChatRequestUserMessage chatRequestMessage = (ChatRequestUserMessage) message; String content = StringEscapeUtils.unescapeXml( - chatRequestMessage.getContent().toString()); + BinaryDataUtils.toString(chatRequestMessage.getContent())); return new ChatRequestUserMessage(content) .setName(chatRequestMessage.getName()); } else if (message instanceof ChatRequestSystemMessage) { ChatRequestSystemMessage chatRequestMessage = (ChatRequestSystemMessage) message; - String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent()); + String content = StringEscapeUtils + .unescapeXml( + BinaryDataUtils.toString(chatRequestMessage.getContent())); return new ChatRequestSystemMessage(content) .setName(chatRequestMessage.getName()); } else if (message instanceof ChatRequestAssistantMessage) { ChatRequestAssistantMessage chatRequestMessage = (ChatRequestAssistantMessage) message; - String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent()); + String content = StringEscapeUtils + .unescapeXml( + BinaryDataUtils.toString(chatRequestMessage.getContent())); return new ChatRequestAssistantMessage(content) .setToolCalls(chatRequestMessage.getToolCalls()) @@ -145,7 +149,9 @@ public static ChatRequestMessage unescapeRequest(ChatRequestMessage message) { content); } else if (message instanceof ChatRequestToolMessage) { ChatRequestToolMessage chatRequestMessage = (ChatRequestToolMessage) message; - String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent()); + String content = StringEscapeUtils + .unescapeXml( + BinaryDataUtils.toString(chatRequestMessage.getContent())); return new ChatRequestToolMessage( content, diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/ChatCompletionsJsonSchemaResponseFormat.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/ChatCompletionsJsonSchemaResponseFormat.java new file mode 100644 index 000000000..f5a3b1c4f --- /dev/null +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/ChatCompletionsJsonSchemaResponseFormat.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion.responseformat; + +import com.azure.ai.openai.models.ChatCompletionsResponseFormat; +import com.azure.json.JsonWriter; +import com.microsoft.semantickernel.orchestration.responseformat.JsonResponseSchema; +import java.io.IOException; + +/** + * Represents a response format for chat completions that uses a JSON schema. + */ +public class ChatCompletionsJsonSchemaResponseFormat extends ChatCompletionsResponseFormat { + + private final JsonResponseSchema schema; + private String type = "json_schema"; + + /** + * Creates a new instance of the {@link ChatCompletionsJsonSchemaResponseFormat} class. + * + * @param schema The JSON schema. + */ + public ChatCompletionsJsonSchemaResponseFormat(JsonResponseSchema schema) { + this.schema = schema; + } + + @Override + public String getType() { + return this.type; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + jsonWriter.writeStringField("type", this.type); + jsonWriter.writeStartObject("json_schema"); + + jsonWriter.writeBooleanField("strict", this.schema.isStrict()); + jsonWriter.writeStringField("name", this.schema.getName()); + + jsonWriter.writeRawField("schema", this.schema.getSchema()); + jsonWriter.writeEndObject(); + return jsonWriter.writeEndObject(); + } + +} diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/JacksonResponseFormatGenerator.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/JacksonResponseFormatGenerator.java new file mode 100644 index 000000000..97060cab0 --- /dev/null +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/JacksonResponseFormatGenerator.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion.responseformat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.victools.jsonschema.generator.OptionPreset; +import com.github.victools.jsonschema.generator.SchemaGenerator; +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; +import com.github.victools.jsonschema.generator.SchemaVersion; +import com.github.victools.jsonschema.module.jackson.JacksonModule; +import com.microsoft.semantickernel.orchestration.responseformat.ResponseSchemaGenerator; + +/** + * Represents a response format generator that uses Jackson. + */ +public class JacksonResponseFormatGenerator implements ResponseSchemaGenerator { + + private final SchemaGenerator generator; + + /** + * Creates a new instance of the {@link JacksonResponseFormatGenerator} class. + */ + public JacksonResponseFormatGenerator() { + JacksonModule module = new JacksonModule(); + SchemaGeneratorConfigBuilder builder = new SchemaGeneratorConfigBuilder( + SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .with(module); + + builder + .forFields() + .withRequiredCheck(fieldScope -> { + return true; + }); + + generator = new SchemaGenerator(builder.build()); + } + + /** + * Creates a new instance of the {@link JacksonResponseFormatGenerator} class. + * + * @param generator The schema generator. + */ + public JacksonResponseFormatGenerator(SchemaGenerator generator) { + this.generator = generator; + } + + @Override + public String generateSchema(Class clazz) { + ObjectNode schema = generator.generateSchema(clazz); + + sanitize(schema); + + return schema.toPrettyString(); + } + + private static void sanitize(ContainerNode schema) { + if (schema instanceof ObjectNode) { + ((ObjectNode) schema).remove("$schema"); + + if (schema.has("type") && schema.get("type").asText().equals("object")) { + ((ObjectNode) schema).put("additionalProperties", false); + } + + for (JsonNode node : (ObjectNode) schema) { + if (node instanceof ContainerNode) { + sanitize((ContainerNode) node); + } + } + } else if (schema instanceof ArrayNode) { + for (JsonNode node : (ArrayNode) schema) { + if (node instanceof ContainerNode) { + sanitize((ContainerNode) node); + } + } + } + } +} diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAIStreamingTextContent.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAIStreamingTextContent.java new file mode 100644 index 000000000..13272fb50 --- /dev/null +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAIStreamingTextContent.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.textcompletion; + +import com.microsoft.semantickernel.services.StreamingTextContent; +import com.microsoft.semantickernel.services.textcompletion.TextContent; + +import javax.annotation.Nullable; + +/** + * StreamingTextContent is a wrapper for TextContent that allows for streaming. + */ +public class OpenAIStreamingTextContent extends StreamingTextContent { + + /** + * Initializes a new instance of the {@code StreamingTextContent} class with a provided text + * content. + * + * @param content The text content. + */ + public OpenAIStreamingTextContent(TextContent content) { + super(content, 0, null, null); + } + + @Override + @Nullable + public String getContent() { + TextContent content = getInnerContent(); + if (content == null) { + return null; + } + return content.getContent(); + } + +} diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java index 13783229d..ec04c568d 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java @@ -11,11 +11,9 @@ import com.microsoft.semantickernel.exceptions.AIException.ErrorCodes; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; -import com.microsoft.semantickernel.services.textcompletion.StreamingTextContent; +import com.microsoft.semantickernel.services.StreamingTextContent; import com.microsoft.semantickernel.services.textcompletion.TextContent; import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; -import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; -import io.opentelemetry.api.trace.Span; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -75,7 +73,7 @@ public Flux getStreamingTextContentsAsync( return this .internalCompleteTextAsync(prompt, executionSettings) .flatMapMany(it -> Flux.fromStream(it.stream()) - .map(StreamingTextContent::new)); + .map(OpenAIStreamingTextContent::new)); } protected Mono> internalCompleteTextAsync( @@ -84,23 +82,14 @@ protected Mono> internalCompleteTextAsync( CompletionsOptions completionsOptions = getCompletionsOptions(text, requestSettings); - Span span = SemanticKernelTelemetry.startTextCompletionSpan( - getModelId(), - SemanticKernelTelemetry.OPEN_AI_PROVIDER, - completionsOptions.getMaxTokens(), - completionsOptions.getTemperature(), - completionsOptions.getTopP()); return getClient() .getCompletionsWithResponse(getDeploymentName(), completionsOptions, OpenAIRequestSettings.getRequestOptions()) .flatMap(completionsResult -> { if (completionsResult.getStatusCode() >= 400) { - SemanticKernelTelemetry.endSpanWithError(span); return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR, "Request failed: " + completionsResult.getStatusCode())); } - SemanticKernelTelemetry.endSpanWithUsage(span, - completionsResult.getValue().getUsage()); return Mono.just(completionsResult.getValue()); }) .map(completions -> { diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textembedding/OpenAITextEmbeddingGenerationService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textembedding/OpenAITextEmbeddingGenerationService.java index cd2c7aa89..355f2ab95 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textembedding/OpenAITextEmbeddingGenerationService.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textembedding/OpenAITextEmbeddingGenerationService.java @@ -8,35 +8,45 @@ import com.microsoft.semantickernel.aiservices.openai.OpenAiService; import com.microsoft.semantickernel.exceptions.AIException; import com.microsoft.semantickernel.services.openai.OpenAiServiceBuilder; -import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; import com.microsoft.semantickernel.services.textembedding.Embedding; import com.microsoft.semantickernel.services.textembedding.TextEmbeddingGenerationService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; - /** * An OpenAI implementation of a {@link TextEmbeddingGenerationService}. - * */ public class OpenAITextEmbeddingGenerationService extends OpenAiService implements TextEmbeddingGenerationService { + private static final Logger LOGGER = LoggerFactory .getLogger(OpenAITextEmbeddingGenerationService.class); - private static final int DEFAULT_DIMENSIONS = 1536; private final int dimensions; + /** + * Dimension of the OpenAI + * {@code text-embedding-3-small} model. + */ + public static final int EMBEDDING_DIMENSIONS_SMALL = 1536; + /** + * Dimension of the OpenAI + * {@code text-embedding-3-large} model. + */ + public static final int EMBEDDING_DIMENSIONS_LARGE = 3072; + /** * Creates a new {@link OpenAITextEmbeddingGenerationService}. * - * @param client OpenAI client + * @param client OpenAI client * @param deploymentName deployment name - * @param modelId OpenAI model id - * @param serviceId Service id + * @param dimensions The dimensions for the embeddings. + * @param modelId OpenAI model id + * @param serviceId Service id */ public OpenAITextEmbeddingGenerationService( OpenAIAsyncClient client, @@ -57,6 +67,24 @@ public static Builder builder() { return new Builder(); } + /** + * Generates embeddings for the given data. + * + * @param data The data to generate embeddings for. + * @return A Mono that completes with the embeddings. + */ + @Override + public Mono generateEmbeddingAsync(String data) { + return this.internalGenerateTextEmbeddingsAsync(Arrays.asList(data)) + .flatMap(embeddings -> { + if (embeddings.isEmpty()) { + return Mono.empty(); + } + + return Mono.just(embeddings.get(0)); + }); + } + /** * Generates embeddings for the given data. * @@ -71,8 +99,10 @@ public Mono> generateEmbeddingsAsync(List data) { protected Mono> internalGenerateTextEmbeddingsAsync(List data) { EmbeddingsOptions options = new EmbeddingsOptions(data) .setModel(getModelId()) - .setDimensions(dimensions) .setInputType("string"); + if (dimensions > 0) { + options.setDimensions(dimensions); + } return getClient() .getEmbeddings(getModelId(), options) @@ -88,7 +118,8 @@ protected Mono> internalGenerateTextEmbeddingsAsync(List */ public static class Builder extends OpenAiServiceBuilder { - private int dimensions = DEFAULT_DIMENSIONS; + + private int dimensions = -1; /** * Sets the dimensions for the embeddings. diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java index c8bde155b..5c3444b3a 100644 --- a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java +++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java @@ -75,57 +75,6 @@ public static void shutdown() { otel.shutdown(); } - @Test - public void otelTextCaptureTest() { - - OpenAIAsyncClient openAIAsyncClient = Mockito.mock(OpenAIAsyncClient.class); - - CompletionsUsage completionsUsage = Mockito.mock(CompletionsUsage.class); - Mockito.when(completionsUsage.getCompletionTokens()).thenReturn(22); - Mockito.when(completionsUsage.getPromptTokens()).thenReturn(55); - - Completions completions = Mockito.mock(Completions.class); - Mockito.when(completions.getUsage()).thenReturn(completionsUsage); - - Response response = Mockito.mock(Response.class); - Mockito.when(response.getStatusCode()).thenReturn(200); - Mockito.when(response.getValue()).thenReturn(completions); - - Mockito.when(openAIAsyncClient.getCompletionsWithResponse( - Mockito.any(), - Mockito.any(), - Mockito.any())).thenAnswer(invocation -> Mono.just(response)); - - TextGenerationService client = OpenAITextGenerationService.builder() - .withOpenAIAsyncClient(openAIAsyncClient) - .withModelId("a-model") - .build(); - - try { - client.getTextContentsAsync( - "foo", - null, - null).block(); - } catch (Exception e) { - // Expect to fail - } - - Assertions.assertFalse(spans.isEmpty()); - Assertions.assertEquals("a-model", - spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.request.model"))); - Assertions.assertEquals("text.completions", - spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name"))); - Assertions.assertEquals("openai", - spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.system"))); - Assertions.assertEquals(22, - spans.get(0).getAttributes() - .get(AttributeKey.longKey("gen_ai.response.completion_tokens"))); - Assertions.assertEquals(55, - spans.get(0).getAttributes() - .get(AttributeKey.longKey("gen_ai.response.prompt_tokens"))); - - } - @Test public void otelChatCaptureTest() { OpenAIAsyncClient openAIAsyncClient = Mockito.mock(OpenAIAsyncClient.class); @@ -169,9 +118,9 @@ public void otelChatCaptureTest() { spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.system"))); Assertions.assertEquals(21, spans.get(0).getAttributes() - .get(AttributeKey.longKey("gen_ai.response.completion_tokens"))); + .get(AttributeKey.longKey("gen_ai.usage.output_tokens"))); Assertions.assertEquals(42, spans.get(0).getAttributes() - .get(AttributeKey.longKey("gen_ai.response.prompt_tokens"))); + .get(AttributeKey.longKey("gen_ai.usage.input_tokens"))); } } diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Bar.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Bar.java new file mode 100644 index 000000000..45d1caa78 --- /dev/null +++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Bar.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Bar { + + private final String bar; + + public Bar( + @JsonProperty("bar") String bar) { + this.bar = bar; + } + + public String getBar() { + return bar; + } + +} diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Baz.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Baz.java new file mode 100644 index 000000000..90ad60ac1 --- /dev/null +++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Baz.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Baz { + + @JsonProperty("bar") + private final Bar bar; + + @JsonCreator + public Baz( + @JsonProperty("bar") Bar bar) { + this.bar = bar; + } + + @JsonProperty("bar") + public Bar getBar() { + return bar; + } +} \ No newline at end of file diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Foo.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Foo.java new file mode 100644 index 000000000..34479b221 --- /dev/null +++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Foo.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Foo { + + @JsonProperty("bar") + private final T bar; + + @JsonCreator + public Foo( + @JsonProperty("bar") T bar) { + this.bar = bar; + } + + @JsonProperty("bar") + public T getBar() { + return bar; + } +} \ No newline at end of file diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/JsonSchemaTest.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/JsonSchemaTest.java new file mode 100644 index 000000000..33870fba2 --- /dev/null +++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/JsonSchemaTest.java @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.semantickernel.orchestration.responseformat.JsonSchemaResponseFormat; +import com.microsoft.semantickernel.plugin.KernelPlugin; +import com.microsoft.semantickernel.plugin.KernelPluginFactory; +import com.microsoft.semantickernel.semanticfunctions.KernelFunction; +import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; +import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +public class JsonSchemaTest { + + @Test + public void jacksonGenerationTest() throws JsonProcessingException { + JsonSchemaResponseFormat format = JsonSchemaResponseFormat.builder() + .setResponseFormat(Foo.class) + .setName("foo") + .build(); + + Assertions.assertEquals("foo", format.getJsonSchema().getName()); + + Assertions.assertTrue(format.getJsonSchema().getSchema() + .replaceAll("\\r\\n|\\r|\\n", "") + .replaceAll(" +", "") + .contains( + "\"type\":\"object\",\"properties\":{\"bar\":{}}")); + } + + @Test + public void openAIFunctionTest() { + KernelPlugin plugin = KernelPluginFactory.createFromObject( + new TestPlugin(), + "test"); + + Assertions.assertNotNull(plugin); + Assertions.assertEquals(plugin.getName(), "test"); + Assertions.assertEquals(plugin.getFunctions().size(), 3); + + KernelFunction testFunction = plugin.getFunctions() + .get("asyncPersonFunction"); + OpenAIFunction openAIFunction = OpenAIFunction.build( + testFunction.getMetadata(), + plugin.getName()); + + String parameters = "{\"type\":\"object\",\"required\":[\"person\",\"input\"],\"properties\":{\"input\":{\"type\":\"string\",\"description\":\"input string\"},\"person\":{\"type\":\"object\",\"properties\":{\"age\":{\"type\":\"integer\",\"description\":\"The age of the person.\"},\"name\":{\"type\":\"string\",\"description\":\"The name of the person.\"},\"title\":{\"type\":\"string\",\"enum\":[\"MS\",\"MRS\",\"MR\"],\"description\":\"The title of the person.\"}},\"required\":[\"age\",\"name\",\"title\"],\"additionalProperties\":false,\"description\":\"input person\"}}}"; + Assertions.assertEquals(parameters, + openAIFunction.getFunctionDefinition().getParameters().toString()); + + } + + public static class TestPlugin { + + @DefineKernelFunction + public String testFunction( + @KernelFunctionParameter(name = "input", description = "input string") String input) { + return "test" + input; + } + + @DefineKernelFunction(returnType = "int") + public Mono asyncTestFunction( + @KernelFunctionParameter(name = "input") String input) { + return Mono.just(1); + } + + @DefineKernelFunction(returnType = "int", description = "test function description", name = "asyncPersonFunction", returnDescription = "test return description") + public Mono asyncPersonFunction( + @KernelFunctionParameter(name = "person", description = "input person", type = Person.class) Person person, + @KernelFunctionParameter(name = "input", description = "input string") String input) { + return Mono.just(1); + } + } + + private static enum Title { + MS, MRS, MR + } + + public static class Person { + @JsonPropertyDescription("The name of the person.") + private String name; + @JsonPropertyDescription("The age of the person.") + private int age; + @JsonPropertyDescription("The title of the person.") + private Title title; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public Title getTitle() { + return title; + } + + public void setTitle(Title title) { + this.title = title; + } + } + +} diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiChatCompletionTest.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiChatCompletionTest.java index 5a25e1aeb..5a1be82a4 100644 --- a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiChatCompletionTest.java +++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiChatCompletionTest.java @@ -9,10 +9,11 @@ import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpRequest; import com.azure.core.http.rest.Response; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.azure.json.JsonOptions; +import com.azure.json.implementation.DefaultJsonReader; import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; import java.nio.charset.Charset; @@ -45,7 +46,7 @@ public void serializesToolCallsCorrectly() { "a-tool-id", "pluginName", "funcName", - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("id", "ca2fc6bc-1307-4da6-a009-d7bf88dec37b") .build())))); chatHistory.addMessage(new OpenAIChatMessageContent( @@ -104,9 +105,9 @@ public ChatCompletions getValue() { String message = EmbeddedResourceLoader.readFile("chatCompletion.txt", OpenAiChatCompletionTest.class); - return new ObjectMapper() - .readValue(String.format(message, "Snuggles"), - ChatCompletions.class); + return ChatCompletions.fromJson( + DefaultJsonReader.fromString( + String.format(message, "Snuggles"), new JsonOptions())); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/api-test/integration-tests/pom.xml b/api-test/integration-tests/pom.xml index 7e669927c..9b5c3883d 100644 --- a/api-test/integration-tests/pom.xml +++ b/api-test/integration-tests/pom.xml @@ -6,7 +6,7 @@ com.microsoft.semantic-kernel api-test - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -58,21 +58,51 @@ com.microsoft.semantic-kernel - semantickernel-experimental + semantickernel-data-jdbc + test + + + com.microsoft.semantic-kernel + semantickernel-data-mysql + test + + + com.microsoft.semantic-kernel + semantickernel-data-hsqldb + test + + + com.microsoft.semantic-kernel + semantickernel-data-sqlite + test + + + com.microsoft.semantic-kernel + semantickernel-data-redis test org.xerial sqlite-jdbc - 3.44.1.0 + 3.46.0.0 - mysql - mysql-connector-java - 8.0.33 + com.mysql + mysql-connector-j + 9.6.0 test + + org.postgresql + postgresql + 42.7.10 + + + org.xerial + sqlite-jdbc + 3.53.0.0 + org.testcontainers @@ -92,13 +122,37 @@ com.redis testcontainers-redis - 2.2.2 + 2.2.4 test org.wiremock wiremock - 3.3.1 + test + + + + + com.github.victools + jsonschema-generator + test + + + com.github.victools + jsonschema-module-jackson + test + + + + + org.hsqldb + hsqldb + 2.7.4 + test + + + com.microsoft.semantic-kernel + semantickernel-api-data test @@ -108,7 +162,7 @@ org.testcontainers testcontainers-bom - 1.18.3 + 2.0.4 pom import diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example03_ArgumentsTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example03_ArgumentsTest.java index ac1821246..dd283952d 100644 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example03_ArgumentsTest.java +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example03_ArgumentsTest.java @@ -6,7 +6,7 @@ import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.syntaxexamples.functions.Example03_Arguments.StaticTextPlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -24,7 +24,7 @@ public void main() { KernelPlugin functionCollection = KernelPluginFactory .createFromObject(new StaticTextPlugin(), "text"); - KernelFunctionArguments arguments = KernelFunctionArguments.builder() + KernelArguments arguments = KernelArguments.builder() .withInput("Today is: ") .withVariable("day", "Monday") .build(); diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example05_InlineFunctionDefinitionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example05_InlineFunctionDefinitionTest.java index f74268b25..d791ec9c2 100644 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example05_InlineFunctionDefinitionTest.java +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example05_InlineFunctionDefinitionTest.java @@ -10,9 +10,9 @@ import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; -import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; + import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; @@ -68,7 +68,7 @@ public void main(WireMockRuntimeInfo wmRuntimeInfo) { var result = kernel.invokeAsync(excuseFunction) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withInput("I missed the F1 final race") .build()) .block(); @@ -79,7 +79,7 @@ public void main(WireMockRuntimeInfo wmRuntimeInfo) { result = kernel.invokeAsync(excuseFunction) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withInput("sorry I forgot your birthday") .build()) .block(); diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ImportingMultiplePluginsTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ImportingMultiplePluginsTest.java index dc7abe14a..e9e5c6d4a 100644 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ImportingMultiplePluginsTest.java +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ImportingMultiplePluginsTest.java @@ -5,10 +5,11 @@ import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; public class ImportingMultiplePluginsTest { - + @Disabled @Test public void canImportMultiplePlugins() { KernelPlugin summarize = KernelPluginFactory.importPluginFromResourcesDirectory( diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/KernelHooksTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/KernelHooksTest.java index 16b9cadaf..4a128fb16 100644 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/KernelHooksTest.java +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/KernelHooksTest.java @@ -12,7 +12,7 @@ import com.microsoft.semantickernel.hooks.KernelHook.FunctionInvokingHook; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.semanticfunctions.OutputVariable; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; @@ -90,7 +90,7 @@ public void getUsageAsync(WireMockRuntimeInfo wmRuntimeInfo) { kernel.invokeAsync( excuseFunction) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable("input", "I missed the F1 final race") .build()) diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java index 39b11182a..ff4655d26 100644 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java @@ -12,7 +12,7 @@ import com.microsoft.semantickernel.aiservices.openai.textcompletion.OpenAITextGenerationService; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; @@ -49,7 +49,7 @@ public void textSemanticKernelTemplateXml() { """) .withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT) .build()) - .withArguments(KernelFunctionArguments + .withArguments(KernelArguments .builder() .withVariable("value", "\"hello world\"") .build()) @@ -71,7 +71,7 @@ public void textSemanticKernelTemplate() { """) .withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT) .build()) - .withArguments(KernelFunctionArguments + .withArguments(KernelArguments .builder() .withVariable("value", "{{$ignore}}") .withVariable("ignore", "dont show") @@ -94,7 +94,7 @@ public void textHandleBarsTemplate() { """) .withTemplateFormat("handlebars") .build()) - .withArguments(KernelFunctionArguments + .withArguments(KernelArguments .builder() .withVariable("value", "{{ignore}}") .withVariable("ignore", "dont show") @@ -117,7 +117,7 @@ public void chatSemanticKernelTemplateXml() { """) .withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT) .build()) - .withArguments(KernelFunctionArguments + .withArguments(KernelArguments .builder() .withVariable("value", "\"hello world\"") .build()) @@ -139,7 +139,7 @@ public void chatSemanticKernelTemplate() { """) .withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT) .build()) - .withArguments(KernelFunctionArguments + .withArguments(KernelArguments .builder() .withVariable("value", "{{$ignore}}") .withVariable("ignore", "dont show") @@ -162,7 +162,7 @@ public void chatHandleBarsTemplate() { """) .withTemplateFormat("handlebars") .build()) - .withArguments(KernelFunctionArguments + .withArguments(KernelArguments .builder() .withVariable("value", "{{ignore}}") .withVariable("ignore", "dont show") @@ -185,7 +185,7 @@ public void chatSemanticKernelTemplate2() { """) .withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT) .build()) - .withArguments(KernelFunctionArguments + .withArguments(KernelArguments .builder() .withVariable("value", "{{$ignore}}") .withVariable("ignore", "dont show") @@ -240,10 +240,10 @@ public void canHandleIterableReturnFromFunction() { .block(); String requestBody = wm.getAllServeEvents().get(0).getRequest().getBodyAsString(); - Assertions.assertTrue(requestBody.contains("{\"role\":\"user\",\"content\":\"[]")); - Assertions.assertTrue(requestBody.contains("{\"role\":\"user\",\"content\":\"[1, 2, 3]")); - Assertions.assertTrue(requestBody.contains("{\"role\":\"user\",\"content\":\"[4, 5, 6]")); - Assertions.assertTrue(requestBody.contains("{\"role\":\"user\",\"content\":\"[7, 8, 9]")); + Assertions.assertTrue(requestBody.contains("\"content\":\"[]")); + Assertions.assertTrue(requestBody.contains("\"content\":\"[1, 2, 3]")); + Assertions.assertTrue(requestBody.contains("\"content\":\"[4, 5, 6]")); + Assertions.assertTrue(requestBody.contains("\"content\":\"[7, 8, 9]")); } private Kernel buildTextKernel() { diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ResponseSchemaTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ResponseSchemaTest.java new file mode 100644 index 000000000..4ec677742 --- /dev/null +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ResponseSchemaTest.java @@ -0,0 +1,311 @@ +package com.microsoft.semantickernel.tests; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.models.ChatCompletions; +import com.azure.ai.openai.models.ChatCompletionsOptions; +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.rest.RequestOptions; +import com.azure.core.http.rest.Response; +import com.azure.json.JsonOptions; +import com.azure.json.JsonWriter; +import com.azure.json.implementation.DefaultJsonReader; +import com.azure.json.implementation.DefaultJsonWriter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.contextvariables.converters.ContextVariableJacksonConverter; +import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader; +import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader.ResourceLocation; +import com.microsoft.semantickernel.orchestration.FunctionResult; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.orchestration.responseformat.JsonSchemaResponseFormat; +import com.microsoft.semantickernel.semanticfunctions.HandlebarsPromptTemplateFactory; +import com.microsoft.semantickernel.semanticfunctions.KernelFunctionYaml; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import java.io.IOException; +import java.io.StringWriter; +import org.apache.commons.text.StringEscapeUtils; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; + +public class ResponseSchemaTest { + + public static class TestClass { + + private final String name; + + @JsonCreator + public TestClass( + @JsonProperty("name") String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + @Test + public void sendsResponseSchemaFromTemplate() throws IOException { + OpenAIAsyncClient client = getOpenAIAsyncClient( + """ + { + "name": "Test name" + } + """ + .stripIndent()); + Kernel kernel = buildKernel(client); + + var getIntent = KernelFunctionYaml.fromPromptYaml( + EmbeddedResourceLoader + .readFile("responseSchema.prompt.yaml", ResponseSchemaTest.class, + ResourceLocation.CLASSPATH_ROOT), + new HandlebarsPromptTemplateFactory()); + + FunctionResult response = getIntent.invokeAsync(kernel) + .withResultTypeAutoConversion(TestClass.class) + .block(); + + verifyCalled(client, + """ + { + "type":"json_schema", + "json_schema":{ + "strict":true, + "name":"Test", + "schema":{ + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + } + }, + "required" : [ + "name" + ], + "additionalProperties" : false + } + } + } + """ + ); + } + + @Disabled + @Test + public void sendsResponseSchema() { + OpenAIAsyncClient client = getOpenAIAsyncClient( + """ + { + "name": "Test name" + } + """ + .stripIndent()); + + Kernel kernel = buildKernel(client); + + PromptExecutionSettings promptExecutionSettings = PromptExecutionSettings.builder() + .withResponseFormat( + JsonSchemaResponseFormat.builder() + .setResponseFormat(TestClass.class) + .setName("Test") + .build() + ) + .build(); + + FunctionResult response = kernel.invokePromptAsync( + "Generate TestClass") + .withTypeConverter(ContextVariableJacksonConverter.create(TestClass.class)) + .withResultType(TestClass.class) + .withPromptExecutionSettings(promptExecutionSettings) + .block(); + + verifyCalled(client, + """ + { + "type":"json_schema", + "json_schema":{ + "strict":true, + "name":"Test", + "schema":{ + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + } + }, + "required" : [ "name" ], + "additionalProperties" : false + } + } + } + """ + ); + } + + private static void verifyCalled(OpenAIAsyncClient client, String expected) { + Mockito.verify(client, Mockito.atLeastOnce()) + .getChatCompletionsWithResponse( + Mockito.any(), + Mockito.argThat( + (ChatCompletionsOptions chatCompletionsOptions) -> { + StringWriter writer = new StringWriter(); + try { + JsonWriter jsonWriter = DefaultJsonWriter.toWriter( + writer, + new JsonOptions() + ); + JsonWriter format = chatCompletionsOptions.getResponseFormat() + .toJson(jsonWriter); + jsonWriter.flush(); + writer.flush(); + + String json = String.valueOf(writer.getBuffer()) + .replaceAll("\n", "") + .replaceAll("\r", "") + .replaceAll(" +", ""); + String expectedClean = expected + .stripIndent() + .replaceAll("\n", "") + .replaceAll("\r", "") + .replaceAll(" +", ""); + + return json.equals(expectedClean); + + } catch (IOException e) { + throw new RuntimeException(e); + } + }), + Mockito.any()); + } + + private @NotNull OpenAIAsyncClient getOpenAIAsyncClient(String response) { + OpenAIAsyncClient client = Mockito.mock(OpenAIAsyncClient.class); + + Mockito.when( + client.getChatCompletionsWithResponse( + Mockito.any(), + Mockito.any(), + Mockito.any())) + .thenReturn( + Mono.just( + new Response() { + @Override + public int getStatusCode() { + return 200; + } + + @Override + public HttpHeaders getHeaders() { + return new HttpHeaders(); + } + + @Override + public HttpRequest getRequest() { + return null; + } + + @Override + public ChatCompletions getValue() { + return buildResponse(response); + } + })); + return client; + } + + + private ChatCompletions buildResponse(String response) { + String str = String.format(""" + { + "choices" : [ + { + "content_filter_results" : { + "hate" : { + "filtered" : false, + "severity" : "safe" + }, + "self_harm" : { + "filtered" : false, + "severity" : "safe" + }, + "sexual" : { + "filtered" : false, + "severity" : "safe" + }, + "violence" : { + "filtered" : false, + "severity" : "safe" + } + }, + "finish_reason" : "stop", + "index" : 0, + "message" : { + "content" : "%s", + "role" : "assistant" + } + } + ], + "created" : 1707253039, + "id" : "chatcmpl-xxx", + "prompt_filter_results" : [ + { + "content_filter_results" : { + "hate" : { + "filtered" : false, + "severity" : "safe" + }, + "self_harm" : { + "filtered" : false, + "severity" : "safe" + }, + "sexual" : { + "filtered" : false, + "severity" : "safe" + }, + "violence" : { + "filtered" : false, + "severity" : "safe" + } + }, + "prompt_index" : 0 + } + ], + "usage" : { + "completion_tokens" : 131, + "prompt_tokens" : 26, + "total_tokens" : 157 + } + } + """, StringEscapeUtils.escapeJson(response)); + + try { + return ChatCompletions.fromJson( + DefaultJsonReader.fromString( + str, new JsonOptions()) + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + private Kernel buildKernel(OpenAIAsyncClient client) { + + ChatCompletionService openAIChatCompletion = OpenAIChatCompletion.builder() + .withOpenAIAsyncClient(client) + .withModelId("a-model") + .build(); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, openAIChatCompletion) + .build(); + + return kernel; + + } +} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java index 852f55784..5f80a6e00 100644 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java @@ -282,7 +282,7 @@ private ChatCompletionService getChatCompletionService() { public static MappingBuilder buildTextResponse(String bodyMatcher, String responseBody) { return post(urlEqualTo( - "//openai/deployments/gpt-35-turbo-2/completions?api-version=2024-03-01-preview")) + "//openai/deployments/gpt-35-turbo-2/completions?api-version=2025-01-01-preview")) .withRequestBody(new ContainsPattern(bodyMatcher)) .willReturn( aResponse() @@ -293,7 +293,7 @@ public static MappingBuilder buildTextResponse(String bodyMatcher, String respon public static MappingBuilder buildResponse(String bodyMatcher, String responseBody) { return post(urlEqualTo( - "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2024-03-01-preview")) + "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2025-01-01-preview")) .withRequestBody(new ContainsPattern(bodyMatcher)) .willReturn( aResponse() diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/WiremockExamplesIT.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/WiremockExamplesIT.java index 0eeabc135..f01b40d16 100644 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/WiremockExamplesIT.java +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/WiremockExamplesIT.java @@ -32,8 +32,8 @@ import java.util.List; import java.util.Queue; import java.util.stream.Stream; -import com.microsoft.semantickernel.samples.syntaxexamples.plugins.Example10_DescribeAllPluginsAndFunctions; -import com.microsoft.semantickernel.samples.syntaxexamples.plugins.Example13_ConversationSummaryPlugin; +import com.microsoft.semantickernel.samples.syntaxexamples.plugin.Example10_DescribeAllPluginsAndFunctions; +import com.microsoft.semantickernel.samples.syntaxexamples.plugin.Example13_ConversationSummaryPlugin; import com.microsoft.semantickernel.samples.syntaxexamples.template.Example06_TemplateLanguage; import com.microsoft.semantickernel.samples.syntaxexamples.template.Example56_TemplateMethodFunctionsWithMultipleArguments; import com.microsoft.semantickernel.samples.syntaxexamples.template.Example64_MultiplePromptTemplates; diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/Hotel.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/Hotel.java deleted file mode 100644 index e43842fcd..000000000 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/Hotel.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.microsoft.semantickernel.tests.connectors.memory; - -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordDataAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordKeyAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordVectorAttribute; - -import java.util.List; - -public class Hotel { - @VectorStoreRecordKeyAttribute - private final String id; - @VectorStoreRecordDataAttribute - private final String name; - @VectorStoreRecordDataAttribute - private final int code; - @VectorStoreRecordDataAttribute(hasEmbedding = true, embeddingFieldName = "descriptionEmbedding") - private final String description; - @VectorStoreRecordVectorAttribute(dimensions = 3) - private final List descriptionEmbedding; - @VectorStoreRecordDataAttribute - private final double rating; - - public Hotel() { - this(null, null, 0, null, null, 0.0); - } - - public Hotel(String id, String name, int code, String description, List descriptionEmbedding, double rating) { - this.id = id; - this.name = name; - this.code = code; - this.description = description; - this.descriptionEmbedding = descriptionEmbedding; - this.rating = rating; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public int getCode() { - return code; - } - - public String getDescription() { - return description; - } - - public List getDescriptionEmbedding() { - return descriptionEmbedding; - } - - public double getRating() { - return rating; - } -} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreRecordCollectionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreRecordCollectionTest.java deleted file mode 100644 index 6e80e4ace..000000000 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreRecordCollectionTest.java +++ /dev/null @@ -1,253 +0,0 @@ -package com.microsoft.semantickernel.tests.connectors.memory.jdbc; - -import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreRecordCollection; -import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreRecordCollectionOptions; -import com.microsoft.semantickernel.connectors.data.jdbc.MySQLVectorStoreQueryProvider; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.tests.connectors.memory.Hotel; -import com.mysql.cj.jdbc.MysqlDataSource; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import javax.annotation.Nonnull; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -@Testcontainers -public class JDBCVectorStoreRecordCollectionTest { - @Container - private static final MySQLContainer CONTAINER = new MySQLContainer<>("mysql:5.7.34"); - private static final String MYSQL_USER = "test"; - private static final String MYSQL_PASSWORD = "test"; - private static MysqlDataSource dataSource; - @BeforeAll - static void setup() { - dataSource = new MysqlDataSource(); - dataSource.setUrl(CONTAINER.getJdbcUrl()); - dataSource.setUser(MYSQL_USER); - dataSource.setPassword(MYSQL_PASSWORD); - } - - private JDBCVectorStoreRecordCollection buildRecordCollection(@Nonnull String collectionName) { - JDBCVectorStoreRecordCollection recordCollection = new JDBCVectorStoreRecordCollection<>( - dataSource, - collectionName, - JDBCVectorStoreRecordCollectionOptions.builder() - .withRecordClass(Hotel.class) - .withQueryProvider(MySQLVectorStoreQueryProvider.builder() - .withDataSource(dataSource) - .build()) - .build()); - - recordCollection.prepareAsync().block(); - recordCollection.createCollectionIfNotExistsAsync().block(); - return recordCollection; - } - - @Test - public void buildRecordCollection() { - assertNotNull(buildRecordCollection("buildTest")); - } - - private List getHotels() { - return List.of( - new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", Arrays.asList(1.0f, 2.0f, 3.0f), 4.0), - new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", Arrays.asList(1.0f, 2.0f, 3.0f), 3.0), - new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", Arrays.asList(1.0f, 2.0f, 3.0f), 5.0), - new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", Arrays.asList(1.0f, 2.0f, 3.0f), 4.0), - new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", Arrays.asList(1.0f, 2.0f, 3.0f), 5.0) - ); - } - - @Test - public void upsertAndGetRecordAsync() { - String collectionName = "upsertAndGetRecordAsync"; - JDBCVectorStoreRecordCollection recordStore = buildRecordCollection(collectionName); - - List hotels = getHotels(); - for (Hotel hotel : hotels) { - recordStore.upsertAsync(hotel, null).block(); - } - - for (Hotel hotel : hotels) { - Hotel retrievedHotel = recordStore.getAsync(hotel.getId(), null).block(); - assertNotNull(retrievedHotel); - assertEquals(hotel.getId(), retrievedHotel.getId()); - } - } - - @Test - public void getBatchAsync() { - String collectionName = "getBatchAsync"; - JDBCVectorStoreRecordCollection recordStore = buildRecordCollection(collectionName); - - List hotels = getHotels(); - for (Hotel hotel : hotels) { - recordStore.upsertAsync(hotel, null).block(); - } - - List keys = new ArrayList<>(); - for (Hotel hotel : hotels) { - keys.add(hotel.getId()); - } - - List retrievedHotels = recordStore.getBatchAsync(keys, null).block(); - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - } - - @Test - public void upsertBatchAndGetBatchAsync() { - String collectionName = "upsertBatchAndGetBatchAsync"; - JDBCVectorStoreRecordCollection recordStore = buildRecordCollection(collectionName); - - List hotels = getHotels(); - recordStore.upsertBatchAsync(hotels, null).block(); - - List keys = new ArrayList<>(); - for (Hotel hotel : hotels) { - keys.add(hotel.getId()); - } - - List retrievedHotels = recordStore.getBatchAsync(keys, null).block(); - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - } - - @Test - public void insertAndReplaceAsync() { - String collectionName = "insertAndReplaceAsync"; - JDBCVectorStoreRecordCollection recordStore = buildRecordCollection(collectionName); - - List hotels = getHotels(); - recordStore.upsertBatchAsync(hotels, null).block(); - recordStore.upsertBatchAsync(hotels, null).block(); - recordStore.upsertBatchAsync(hotels, null).block(); - - List keys = new ArrayList<>(); - for (Hotel hotel : hotels) { - keys.add(hotel.getId()); - } - - List retrievedHotels = recordStore.getBatchAsync(keys, null).block(); - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - } - - @Test - public void deleteRecordAsync() { - String collectionName = "deleteRecordAsync"; - JDBCVectorStoreRecordCollection recordStore = buildRecordCollection(collectionName); - - List hotels = getHotels(); - recordStore.upsertBatchAsync(hotels, null).block(); - - for (Hotel hotel : hotels) { - recordStore.deleteAsync(hotel.getId(), null).block(); - Hotel retrievedHotel = recordStore.getAsync(hotel.getId(), null).block(); - assertNull(retrievedHotel); - } - } - - @Test - public void deleteBatchAsync() { - String collectionName = "deleteBatchAsync"; - JDBCVectorStoreRecordCollection recordStore = buildRecordCollection(collectionName); - - List hotels = getHotels(); - recordStore.upsertBatchAsync(hotels, null).block(); - - List keys = new ArrayList<>(); - for (Hotel hotel : hotels) { - keys.add(hotel.getId()); - } - - recordStore.deleteBatchAsync(keys, null).block(); - - for (String key : keys) { - Hotel retrievedHotel = recordStore.getAsync(key, null).block(); - assertNull(retrievedHotel); - } - } - - @Test - public void getWithNoVectors() { - String collectionName = "getWithNoVectors"; - JDBCVectorStoreRecordCollection recordStore = buildRecordCollection(collectionName); - - List hotels = getHotels(); - recordStore.upsertBatchAsync(hotels, null).block(); - - GetRecordOptions options = GetRecordOptions.builder() - .includeVectors(false) - .build(); - - for (Hotel hotel : hotels) { - Hotel retrievedHotel = recordStore.getAsync(hotel.getId(), options).block(); - assertNotNull(retrievedHotel); - assertEquals(hotel.getId(), retrievedHotel.getId()); - assertNull(retrievedHotel.getDescriptionEmbedding()); - } - - options = GetRecordOptions.builder() - .includeVectors(true) - .build(); - - for (Hotel hotel : hotels) { - Hotel retrievedHotel = recordStore.getAsync(hotel.getId(), options).block(); - assertNotNull(retrievedHotel); - assertEquals(hotel.getId(), retrievedHotel.getId()); - assertNotNull(retrievedHotel.getDescriptionEmbedding()); - } - } - - @Test - public void getBatchWithNoVectors() { - String collectionName = "getBatchWithNoVectors"; - JDBCVectorStoreRecordCollection recordStore = buildRecordCollection(collectionName); - - List hotels = getHotels(); - recordStore.upsertBatchAsync(hotels, null).block(); - - GetRecordOptions options = GetRecordOptions.builder() - .includeVectors(false) - .build(); - - List keys = new ArrayList<>(); - for (Hotel hotel : hotels) { - keys.add(hotel.getId()); - } - - List retrievedHotels = recordStore.getBatchAsync(keys, options).block(); - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - - for (Hotel hotel : retrievedHotels) { - assertNull(hotel.getDescriptionEmbedding()); - } - - options = GetRecordOptions.builder() - .includeVectors(true) - .build(); - - retrievedHotels = recordStore.getBatchAsync(keys, options).block(); - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - - for (Hotel hotel : retrievedHotels) { - assertNotNull(hotel.getDescriptionEmbedding()); - } - } -} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreTest.java deleted file mode 100644 index eb134dd04..000000000 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.microsoft.semantickernel.tests.connectors.memory.jdbc; - -import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStore; -import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreOptions; -import com.microsoft.semantickernel.connectors.data.jdbc.MySQLVectorStoreQueryProvider; -import com.microsoft.semantickernel.tests.connectors.memory.Hotel; -import com.mysql.cj.jdbc.MysqlDataSource; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@Testcontainers -public class JDBCVectorStoreTest { - @Container - private static final MySQLContainer CONTAINER = new MySQLContainer<>("mysql:5.7.34"); - private static final String MYSQL_USER = "test"; - private static final String MYSQL_PASSWORD = "test"; - private static MysqlDataSource dataSource; - - @BeforeAll - static void setup() { - dataSource = new MysqlDataSource(); - dataSource.setUrl(CONTAINER.getJdbcUrl()); - dataSource.setUser(MYSQL_USER); - dataSource.setPassword(MYSQL_PASSWORD); - } - - @Test - public void getCollectionNamesAsync() { - MySQLVectorStoreQueryProvider queryProvider = MySQLVectorStoreQueryProvider.builder() - .withDataSource(dataSource) - .build(); - - JDBCVectorStore vectorStore = JDBCVectorStore.builder() - .withDataSource(dataSource) - .withOptions( - JDBCVectorStoreOptions.builder() - .withQueryProvider(queryProvider) - .build() - ) - .build(); - - vectorStore.getCollectionNamesAsync().block(); - - List collectionNames = Arrays.asList("collection1", "collection2", "collection3"); - - for (String collectionName : collectionNames) { - vectorStore.getCollection(collectionName, Hotel.class, null).createCollectionAsync().block(); - } - - List retrievedCollectionNames = vectorStore.getCollectionNamesAsync().block(); - assertNotNull(retrievedCollectionNames); - assertEquals(collectionNames.size(), retrievedCollectionNames.size()); - for (String collectionName : collectionNames) { - assertTrue(retrievedCollectionNames.contains(collectionName)); - } - } -} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreRecordCollectionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreRecordCollectionTest.java deleted file mode 100644 index db3b6d6cd..000000000 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreRecordCollectionTest.java +++ /dev/null @@ -1,305 +0,0 @@ -package com.microsoft.semantickernel.tests.connectors.memory.redis; - -import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStoreRecordCollection; -import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStoreRecordCollectionOptions; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDataField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordKeyField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordVectorField; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.tests.connectors.memory.Hotel; -import com.redis.testcontainers.RedisContainer; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import redis.clients.jedis.JedisPooled; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -@Testcontainers -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class RedisVectorStoreRecordCollectionTest { - - @Container private static final RedisContainer redisContainer = new RedisContainer("redis/redis-stack:latest"); - - private static final Map> optionsMap = new HashMap<>(); - - public enum RecordCollectionOptions { - DEFAULT, WITH_CUSTOM_DEFINITION - } - - @BeforeAll - static void setup() { - optionsMap.put(RecordCollectionOptions.DEFAULT, RedisVectorStoreRecordCollectionOptions.builder() - .withRecordClass(Hotel.class) - .build()); - - List fields = new ArrayList<>(); - fields.add(VectorStoreRecordKeyField.builder() - .withName("id") - .build()); - fields.add(VectorStoreRecordDataField.builder() - .withName("name") - .withFieldType(String.class) - .build()); - fields.add(VectorStoreRecordDataField.builder() - .withName("code") - .withFieldType(Integer.class) - .build()); - fields.add(VectorStoreRecordDataField.builder() - .withName("description") - .withFieldType(String.class) - .withHasEmbedding(true) - .withEmbeddingFieldName("descriptionEmbedding") - .build()); - fields.add(VectorStoreRecordVectorField.builder() - .withName("descriptionEmbedding") - .withDimensions(768) - .build()); - fields.add(VectorStoreRecordDataField.builder() - .withName("rating") - .withFieldType(Double.class) - .build()); - VectorStoreRecordDefinition recordDefinition = VectorStoreRecordDefinition.fromFields(fields); - - optionsMap.put(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, RedisVectorStoreRecordCollectionOptions.builder() - .withRecordClass(Hotel.class) - .withRecordDefinition(recordDefinition) - .build()); - } - - private RedisVectorStoreRecordCollection buildrecordCollection(@Nonnull RedisVectorStoreRecordCollectionOptions options, @Nonnull String collectionName) { - return new RedisVectorStoreRecordCollection<>(new JedisPooled(redisContainer.getRedisURI()), collectionName, RedisVectorStoreRecordCollectionOptions.builder() - .withRecordClass(options.getRecordClass()) - .withVectorStoreRecordMapper(options.getVectorStoreRecordMapper()) - .withRecordDefinition(options.getRecordDefinition()) - .withPrefixCollectionName(options.isPrefixCollectionName()) - .build()); - } - - private List getHotels() { - return List.of( - new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", Arrays.asList(1.0f, 2.0f, 3.0f), 4.0), - new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", Arrays.asList(1.0f, 2.0f, 3.0f), 3.0), - new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", Arrays.asList(1.0f, 2.0f, 3.0f), 5.0), - new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", Arrays.asList(1.0f, 2.0f, 3.0f), 4.0), - new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", Arrays.asList(1.0f, 2.0f, 3.0f), 5.0) - ); - } - - @Order(1) - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void buildrecordCollection(RecordCollectionOptions options) { - assertNotNull(buildrecordCollection(optionsMap.get(options), options.name())); - } - - @Order(2) - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void createCollectionAsync(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - assertEquals(false, recordCollection.collectionExistsAsync().block()); - recordCollection.createCollectionAsync().block(); - assertEquals(true, recordCollection.collectionExistsAsync().block()); - } - - @Test - public void deleteCollectionAsync() { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(RecordCollectionOptions.DEFAULT), "deleteCollectionAsync"); - - assertEquals(false, recordCollection.collectionExistsAsync().block()); - recordCollection.createCollectionAsync().block(); - recordCollection.deleteCollectionAsync().block(); - assertEquals(false, recordCollection.collectionExistsAsync().block()); - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void upsertAndGetRecordAsync(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - for (Hotel hotel : hotels) { - recordCollection.upsertAsync(hotel, null).block(); - } - - for (Hotel hotel : hotels) { - Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); - assertNotNull(retrievedHotel); - assertEquals(hotel.getId(), retrievedHotel.getId()); - } - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void getBatchAsync(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - for (Hotel hotel : hotels) { - recordCollection.upsertAsync(hotel, null).block(); - } - - List ids = new ArrayList<>(); - hotels.forEach(hotel -> ids.add(hotel.getId())); - - List retrievedHotels = recordCollection.getBatchAsync(ids, null).block(); - - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - for (int i = 0; i < hotels.size(); i++) { - assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); - } - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void upsertBatchAsync(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - List keys = recordCollection.upsertBatchAsync(hotels, null).block(); - assertNotNull(keys); - - List retrievedHotels = (List) recordCollection.getBatchAsync(keys, null).block(); - - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - for (int i = 0; i < hotels.size(); i++) { - assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); - } - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void deleteAsync(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - recordCollection.upsertBatchAsync(hotels, null).block(); - - for (Hotel hotel : hotels) { - recordCollection.deleteAsync(hotel.getId(), null).block(); - Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); - assertNull(retrievedHotel); - } - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void deleteBatchAsync(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - recordCollection.upsertBatchAsync(hotels, null).block(); - - List ids = new ArrayList<>(); - hotels.forEach(hotel -> ids.add(hotel.getId())); - - recordCollection.deleteBatchAsync(ids, null).block(); - - for (String id : ids) { - Hotel retrievedHotel = recordCollection.getAsync(id, null).block(); - assertNull(retrievedHotel); - } - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void getAsyncWithVectors(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - recordCollection.upsertBatchAsync(hotels, null).block(); - - for (Hotel hotel : hotels) { - Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); - assertNotNull(retrievedHotel); - assertNotNull(retrievedHotel.getDescriptionEmbedding()); - assertEquals(hotel.getId(), retrievedHotel.getId()); - assertEquals(hotel.getDescription(), retrievedHotel.getDescription()); - } - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void getBatchAsyncWithVectors(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - recordCollection.upsertBatchAsync(hotels, null).block(); - - List ids = new ArrayList<>(); - hotels.forEach(hotel -> ids.add(hotel.getId())); - - List retrievedHotels = recordCollection.getBatchAsync(ids, null).block(); - - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - for (int i = 0; i < hotels.size(); i++) { - assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); - assertEquals(hotels.get(i).getDescription(), retrievedHotels.get(i).getDescription()); - assertNotNull(retrievedHotels.get(i).getDescriptionEmbedding()); - } - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void getAsyncWithNoVectors(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - recordCollection.upsertBatchAsync(hotels, null).block(); - - GetRecordOptions getRecordOptions = GetRecordOptions.builder().includeVectors(false).build(); - for (Hotel hotel : hotels) { - Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), getRecordOptions).block(); - assertNotNull(retrievedHotel); - assertNull(retrievedHotel.getDescriptionEmbedding()); - assertEquals(hotel.getId(), retrievedHotel.getId()); - assertEquals(hotel.getDescription(), retrievedHotel.getDescription()); - } - } - - @ParameterizedTest - @EnumSource(RecordCollectionOptions.class) - public void getBatchAsyncWithNoVectors(RecordCollectionOptions options) { - RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name()); - - List hotels = getHotels(); - recordCollection.upsertBatchAsync(hotels, null).block(); - - GetRecordOptions getRecordOptions = GetRecordOptions.builder().includeVectors(false).build(); - List ids = new ArrayList<>(); - hotels.forEach(hotel -> ids.add(hotel.getId())); - - List retrievedHotels = recordCollection.getBatchAsync(ids, getRecordOptions).block(); - - assertNotNull(retrievedHotels); - assertEquals(hotels.size(), retrievedHotels.size()); - for (int i = 0; i < hotels.size(); i++) { - assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); - assertEquals(hotels.get(i).getDescription(), retrievedHotels.get(i).getDescription()); - assertNull(retrievedHotels.get(i).getDescriptionEmbedding()); - } - } -} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreTest.java deleted file mode 100644 index 96341053e..000000000 --- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.microsoft.semantickernel.tests.connectors.memory.redis; - -import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStore; -import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStoreOptions; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.tests.connectors.memory.Hotel; -import com.redis.testcontainers.RedisContainer; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import redis.clients.jedis.JedisPooled; - -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@Testcontainers -public class RedisVectorStoreTest { - @Container - private static final RedisContainer redisContainer = new RedisContainer("redis/redis-stack:latest"); - private static JedisPooled jedis; - - @BeforeAll - public static void setUp() { - jedis = new JedisPooled(redisContainer.getRedisURI()); - } - - @Test - public void getCollectionNamesAsync() { - RedisVectorStore vectorStore = new RedisVectorStore(jedis, new RedisVectorStoreOptions()); - List collectionNames = Arrays.asList("collection1", "collection2", "collection3"); - - for (String collectionName : collectionNames) { - vectorStore.getCollection(collectionName, Hotel.class, null).createCollectionAsync().block(); - } - - List retrievedCollectionNames = vectorStore.getCollectionNamesAsync().block(); - assertNotNull(retrievedCollectionNames); - assertEquals(collectionNames.size(), retrievedCollectionNames.size()); - for (String collectionName : collectionNames) { - assertTrue(retrievedCollectionNames.contains(collectionName)); - } - } -} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/Hotel.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/Hotel.java new file mode 100644 index 000000000..6b33c380b --- /dev/null +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/Hotel.java @@ -0,0 +1,113 @@ +package com.microsoft.semantickernel.tests.data.jdbc; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; + +import java.util.List; + +public class Hotel { + @JsonProperty("hotelId") + @VectorStoreRecordKey + private final String id; + + @VectorStoreRecordData(isFilterable = true) + private final String name; + + @VectorStoreRecordData + private final int code; + + @JsonProperty("summary") + @VectorStoreRecordData() + private final String description; + + @JsonProperty("summaryEmbedding1") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.EUCLIDEAN_DISTANCE) + private final List euclidean; + + @JsonProperty("summaryEmbedding2") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE) + private final List cosineDistance; + + @JsonProperty("summaryEmbedding3") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.DOT_PRODUCT) + private final List dotProduct; + + @JsonProperty("indexedSummaryEmbedding") + @VectorStoreRecordVector(dimensions = 8, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.EUCLIDEAN_DISTANCE) + private final List indexedEuclidean; + + @VectorStoreRecordData(isFilterable = true) + private final List tags; + + @VectorStoreRecordData + private double rating; + + public Hotel() { + this(null, null, 0, null, null, null, null, null, 0.0, null); + } + + @JsonCreator + public Hotel( + @JsonProperty("hotelId") String id, + @JsonProperty("name") String name, + @JsonProperty("code") int code, + @JsonProperty("summary") String description, + @JsonProperty("summaryEmbedding1") List euclidean, + @JsonProperty("summaryEmbedding2") List cosineDistance, + @JsonProperty("summaryEmbedding3") List dotProduct, + @JsonProperty("indexedSummaryEmbedding") List indexedEuclidean, + @JsonProperty("rating") double rating, + @JsonProperty("tags") List tags) { + this.id = id; + this.name = name; + this.code = code; + this.description = description; + this.euclidean = euclidean; + this.cosineDistance = euclidean; + this.dotProduct = euclidean; + this.indexedEuclidean = euclidean; + this.rating = rating; + this.tags = tags; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + public List getEuclidean() { + return euclidean; + } + + public List getIndexedEuclidean() { + return indexedEuclidean; + } + + public double getRating() { + return rating; + } + + public List getTags() { + return tags; + } + + public void setRating(double rating) { + this.rating = rating; + } +} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreRecordCollectionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreRecordCollectionTest.java new file mode 100644 index 000000000..6bc99f25d --- /dev/null +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreRecordCollectionTest.java @@ -0,0 +1,551 @@ +package com.microsoft.semantickernel.tests.data.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.microsoft.semantickernel.data.jdbc.hsqldb.HSQLDBVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollection; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.mysql.MySQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.sqlite.SQLiteVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.mysql.cj.jdbc.MysqlDataSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.sql.DataSource; +import org.hsqldb.jdbc.JDBCDataSourceFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.postgresql.ds.PGSimpleDataSource; +import org.sqlite.SQLiteDataSource; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + + +@Testcontainers +public class JDBCVectorStoreRecordCollectionTest { + + @Container + private static final MySQLContainer MYSQL_CONTAINER = new MySQLContainer<>("mysql:5.7.34"); + + private static final DockerImageName PGVECTOR = DockerImageName.parse("pgvector/pgvector:pg16") + .asCompatibleSubstituteFor("postgres"); + @Container + private static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer<>( + PGVECTOR); + + public enum QueryProvider { + MySQL, + PostgreSQL, + SQLite, + HSQLDB + } + + static Path createTempDbFile(String prefix) { + try { + Path file = Files.createTempFile(prefix, ".db"); + file.toFile().deleteOnExit(); + return file; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private JDBCVectorStoreRecordCollection buildRecordCollection(QueryProvider provider, + @Nonnull String collectionName) { + SQLVectorStoreQueryProvider queryProvider; + DataSource dataSource; + + switch (provider) { + case MySQL: + MysqlDataSource mysqlDataSource = new MysqlDataSource(); + mysqlDataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl()); + mysqlDataSource.setUser(MYSQL_CONTAINER.getUsername()); + mysqlDataSource.setPassword(MYSQL_CONTAINER.getPassword()); + dataSource = mysqlDataSource; + queryProvider = MySQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + break; + case PostgreSQL: + PGSimpleDataSource pgSimpleDataSource = new PGSimpleDataSource(); + pgSimpleDataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl()); + pgSimpleDataSource.setUser(POSTGRESQL_CONTAINER.getUsername()); + pgSimpleDataSource.setPassword(POSTGRESQL_CONTAINER.getPassword()); + dataSource = pgSimpleDataSource; + queryProvider = PostgreSQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + break; + case SQLite: + Path sqliteDb = createTempDbFile("sqliteDb"); + SQLiteDataSource sqliteDataSource = new SQLiteDataSource(); + sqliteDataSource.setUrl("jdbc:sqlite:file:" + sqliteDb.toFile().getAbsolutePath()); + dataSource = sqliteDataSource; + + queryProvider = SQLiteVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + break; + case HSQLDB: + try { + Path file = createTempDbFile("testHSQLDB"); + + Properties properties = new Properties(); + properties.putAll( + Map.of( + "url", "jdbc:hsqldb:file:" + file.toFile().getAbsolutePath() + + ";sql.syntax_mys=true", + "user", "SA", + "password", "" + ) + ); + + dataSource = JDBCDataSourceFactory.createDataSource(properties); + } catch (Exception e) { + throw new RuntimeException(e); + } + + queryProvider = HSQLDBVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + break; + default: + throw new IllegalArgumentException("Unknown query provider: " + provider); + } + + JDBCVectorStoreRecordCollection recordCollection = new JDBCVectorStoreRecordCollection<>( + dataSource, + collectionName, + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .withQueryProvider(queryProvider) + .build()); + + recordCollection.prepareAsync().block(); + recordCollection.createCollectionIfNotExistsAsync().block(); + return recordCollection; + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void buildRecordCollection(QueryProvider provider) { + assertNotNull(buildRecordCollection(provider, "buildTest")); + } + + private List getHotels() { + return Arrays.asList( + new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", + Arrays.asList(0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f), null, null, null, + 4.0, Arrays.asList("luxury", "city")), + new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", + Arrays.asList(-2.0f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f), null, null, null, + 4.0, Arrays.asList("luxury", "city")), + new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", + Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.3f), null, null, null, + 5.0, Arrays.asList("luxury", "beach")), + new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", + Arrays.asList(7.0f, 1.2f, -5.3f, 2.5f, 6.6f, -7.8f, 3.9f, -0.1f), null, null, null, + 4.0, Arrays.asList("luxury", "city")), + new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", + Arrays.asList(-3.5f, 4.4f, -1.2f, 9.9f, 5.7f, -6.1f, 7.8f, -2.0f), null, null, null, + 4.0, Arrays.asList("luxury", "city")) + ); + } + + /** + * Search embeddings similar to the third hotel embeddings. In order of similarity: 1. Hotel 3 + * 2. Hotel 1 3. Hotel 4 + */ + private static final List SEARCH_EMBEDDINGS = Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, + -0.8f, 1.1f, -2.2f, 8.2f); + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void upsertAndGetRecordAsync(QueryProvider provider) { + String collectionName = "upsertAndGetRecordAsync"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + for (Hotel hotel : hotels) { + recordCollection.upsertAsync(hotel, null).block(); + } + + // Upsert the first time + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNotNull(retrievedHotel); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertEquals(hotel.getRating(), retrievedHotel.getRating()); + + // Update the rating + hotel.setRating(1.0); + } + + // Upsert the second time with updated rating + for (Hotel hotel : hotels) { + recordCollection.upsertAsync(hotel, null).block(); + } + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNotNull(retrievedHotel); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertEquals(1.0, retrievedHotel.getRating()); + } + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void getBatchAsync(QueryProvider provider) { + String collectionName = "getBatchAsync"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + for (Hotel hotel : hotels) { + recordCollection.upsertAsync(hotel, null).block(); + } + + List keys = new ArrayList<>(); + for (Hotel hotel : hotels) { + keys.add(hotel.getId()); + } + + List retrievedHotels = recordCollection.getBatchAsync(keys, null).block(); + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void upsertBatchAndGetBatchAsync(QueryProvider provider) { + String collectionName = "upsertBatchAndGetBatchAsync"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List keys = new ArrayList<>(); + for (Hotel hotel : hotels) { + keys.add(hotel.getId()); + } + + List retrievedHotels = recordCollection.getBatchAsync(keys, null).block(); + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void insertAndReplaceAsync(QueryProvider provider) { + String collectionName = "insertAndReplaceAsync"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + recordCollection.upsertBatchAsync(hotels, null).block(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List keys = new ArrayList<>(); + for (Hotel hotel : hotels) { + keys.add(hotel.getId()); + } + + List retrievedHotels = recordCollection.getBatchAsync(keys, null).block(); + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void deleteRecordAsync(QueryProvider provider) { + String collectionName = "deleteRecordAsync"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + for (Hotel hotel : hotels) { + recordCollection.deleteAsync(hotel.getId(), null).block(); + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNull(retrievedHotel); + } + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void deleteBatchAsync(QueryProvider provider) { + String collectionName = "deleteBatchAsync"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List keys = new ArrayList<>(); + for (Hotel hotel : hotels) { + keys.add(hotel.getId()); + } + + recordCollection.deleteBatchAsync(keys, null).block(); + + for (String key : keys) { + Hotel retrievedHotel = recordCollection.getAsync(key, null).block(); + assertNull(retrievedHotel); + } + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void getWithNoVectors(QueryProvider provider) { + String collectionName = "getWithNoVectors"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + GetRecordOptions options = GetRecordOptions.builder() + .includeVectors(false) + .build(); + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), options).block(); + assertNotNull(retrievedHotel); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertNull(retrievedHotel.getEuclidean()); + } + + options = GetRecordOptions.builder() + .includeVectors(true) + .build(); + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), options).block(); + assertNotNull(retrievedHotel); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertNotNull(retrievedHotel.getEuclidean()); + } + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void getBatchWithNoVectors(QueryProvider provider) { + String collectionName = "getBatchWithNoVectors"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + GetRecordOptions options = GetRecordOptions.builder() + .includeVectors(false) + .build(); + + List keys = new ArrayList<>(); + for (Hotel hotel : hotels) { + keys.add(hotel.getId()); + } + + List retrievedHotels = recordCollection.getBatchAsync(keys, options).block(); + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + + for (Hotel hotel : retrievedHotels) { + assertNull(hotel.getEuclidean()); + } + + options = GetRecordOptions.builder() + .includeVectors(true) + .build(); + + retrievedHotels = recordCollection.getBatchAsync(keys, options).block(); + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + + for (Hotel hotel : retrievedHotels) { + assertNotNull(hotel.getEuclidean()); + } + } + + private static Stream provideSearchParameters() { + return Arrays.stream(QueryProvider.values()).map(provider -> + Stream.of( + Arguments.of(provider, "euclidean"), + Arguments.of(provider, "cosineDistance"), + Arguments.of(provider, "dotProduct") + ) + ).flatMap(s -> s); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void exactSearch(QueryProvider provider, String embeddingName) { + String collectionName = "search" + embeddingName; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withTop(3) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, + options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The third hotel should be the most similar + assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId()); + + options = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withSkip(1) + .withTop(-100) + .build(); + + // Skip the first result + results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(1, results.size()); + // The first hotel should be the most similar + assertEquals(hotels.get(0).getId(), results.get(0).getRecord().getId()); + } + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void approximateSearch(QueryProvider provider) { + String collectionName = "searchWithIndex"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName("indexedEuclidean") + .withTop(5) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, + options).block().getResults(); + assertNotNull(results); + assertEquals(5, results.size()); + // The third hotel should be the most similar + assertEquals("id_3", results.get(0).getRecord().getId()); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void searchWithFilterEqualToFilter(QueryProvider provider, String embeddingName) { + String collectionName = "searchWithFilterEqualToFilter"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withTop(3) + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("rating", 4.0).build()) + .build(); + + // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, + options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The first hotel should be the most similar + assertEquals("id_1", results.get(0).getRecord().getId()); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void searchWithAnyTagEqualToFilter(QueryProvider provider, String embeddingName) { + String collectionName = "searchWithAnyTagEqualToFilter"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, + collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withTop(3) + .withVectorSearchFilter( + VectorSearchFilter.builder() + .anyTagEqualTo("tags", "city").build()) + .build(); + + // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, + options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The first hotel should be the most similar + assertEquals("id_1", results.get(0).getRecord().getId()); + } + + // MySQL will always return the vectors as they're needed to compute the distances + @Test + public void postgresSearchIncludeAndNotIncludeVectors() { + String collectionName = "searchIncludeAndNotIncludeVectors"; + JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection( + QueryProvider.PostgreSQL, collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, + null).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The third hotel should be the most similar + assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId()); + assertNull(results.get(0).getRecord().getEuclidean()); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withIncludeVectors(true) + .build(); + + results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The third hotel should be the most similar + assertEquals("id_3", results.get(0).getRecord().getId()); + assertNotNull(results.get(0).getRecord().getEuclidean()); + } +} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreTest.java new file mode 100644 index 000000000..ce62c70d8 --- /dev/null +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreTest.java @@ -0,0 +1,148 @@ +package com.microsoft.semantickernel.tests.data.jdbc; + +import com.microsoft.semantickernel.data.jdbc.hsqldb.HSQLDBVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.mysql.MySQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.sqlite.SQLiteVectorStoreQueryProvider; +import com.mysql.cj.jdbc.MysqlDataSource; +import org.hsqldb.jdbc.JDBCDataSourceFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.postgresql.ds.PGSimpleDataSource; +import org.sqlite.SQLiteDataSource; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import javax.sql.DataSource; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import com.microsoft.semantickernel.tests.data.jdbc.JDBCVectorStoreRecordCollectionTest.QueryProvider; + +import static com.microsoft.semantickernel.tests.data.jdbc.JDBCVectorStoreRecordCollectionTest.createTempDbFile; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +public class JDBCVectorStoreTest { + + @Container + private static final MySQLContainer MYSQL_CONTAINER = new MySQLContainer<>("mysql:5.7.34"); + + private static final DockerImageName PGVECTOR = DockerImageName.parse("pgvector/pgvector:pg16") + .asCompatibleSubstituteFor("postgres"); + @Container + private static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer<>( + PGVECTOR); + + private JDBCVectorStore buildVectorStore(QueryProvider provider) { + SQLVectorStoreQueryProvider queryProvider; + DataSource dataSource; + + switch (provider) { + case MySQL: + MysqlDataSource mysqlDataSource = new MysqlDataSource(); + mysqlDataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl()); + mysqlDataSource.setUser(MYSQL_CONTAINER.getUsername()); + mysqlDataSource.setPassword(MYSQL_CONTAINER.getPassword()); + dataSource = mysqlDataSource; + queryProvider = MySQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + break; + case PostgreSQL: + PGSimpleDataSource pgSimpleDataSource = new PGSimpleDataSource(); + pgSimpleDataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl()); + pgSimpleDataSource.setUser(POSTGRESQL_CONTAINER.getUsername()); + pgSimpleDataSource.setPassword(POSTGRESQL_CONTAINER.getPassword()); + dataSource = pgSimpleDataSource; + queryProvider = PostgreSQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + break; + case SQLite: + Path sqliteDb = createTempDbFile("testSQLite"); + SQLiteDataSource sqliteDataSource = new SQLiteDataSource(); + sqliteDataSource.setUrl("jdbc:sqlite:file:" + sqliteDb.toFile().getAbsolutePath()); + dataSource = sqliteDataSource; + + queryProvider = SQLiteVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + break; + case HSQLDB: + try { + Path file = createTempDbFile("testHSQLDB"); + + Properties properties = new Properties(); + properties.putAll( + Map.of( + "url", "jdbc:hsqldb:file:" + file.toFile().getAbsolutePath() + + ";sql.syntax_mys=true", + "user", "SA", + "password", "" + ) + ); + + dataSource = JDBCDataSourceFactory.createDataSource(properties); + } catch (Exception e) { + throw new RuntimeException(e); + } + + queryProvider = HSQLDBVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + break; + default: + throw new IllegalArgumentException("Unknown query provider: " + provider); + } + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(dataSource) + .withOptions( + JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build() + ) + .build(); + + vectorStore.prepareAsync().block(); + return vectorStore; + } + + + @ParameterizedTest + @EnumSource(QueryProvider.class) + public void getCollectionNamesAsync(QueryProvider provider) { + JDBCVectorStore vectorStore = buildVectorStore(provider); + + vectorStore.getCollectionNamesAsync().block(); + + List collectionNames = Arrays.asList("collection1", "collection2", "collection3"); + + for (String collectionName : collectionNames) { + vectorStore.getCollection(collectionName, + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()).createCollectionAsync().block(); + } + + List retrievedCollectionNames = vectorStore.getCollectionNamesAsync().block(); + assertNotNull(retrievedCollectionNames); + assertEquals(collectionNames.size(), retrievedCollectionNames.size()); + for (String collectionName : collectionNames) { + assertTrue(retrievedCollectionNames.contains(collectionName)); + } + } +} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/Hotel.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/Hotel.java new file mode 100644 index 000000000..46083425a --- /dev/null +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/Hotel.java @@ -0,0 +1,102 @@ +package com.microsoft.semantickernel.tests.data.redis; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; + +import java.util.List; + +public class Hotel { + + @VectorStoreRecordKey + private final String id; + + @VectorStoreRecordData(isFilterable = true) + private final String name; + + @VectorStoreRecordData(isFilterable = true) + private final int code; + + @JsonProperty("summary") + @VectorStoreRecordData(isFilterable = true) + private final String description; + + @JsonProperty("summaryEmbedding1") + @VectorStoreRecordVector(dimensions = 8, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.EUCLIDEAN_DISTANCE) + private final List euclidean; + + @JsonProperty("summaryEmbedding2") + @VectorStoreRecordVector(dimensions = 8) + private final List cosineDistance; + + @JsonProperty("summaryEmbedding3") + @VectorStoreRecordVector(dimensions = 8, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.DOT_PRODUCT) + private final List dotProduct; + + @VectorStoreRecordData(isFilterable = true) + private double rating; + + public Hotel() { + this(null, null, 0, null, null, null, null, 0.0); + } + + @JsonCreator + public Hotel( + @JsonProperty("id") String id, + @JsonProperty("name") String name, + @JsonProperty("code") int code, + @JsonProperty("summary") String description, + @JsonProperty("summaryEmbedding1") List euclidean, + @JsonProperty("summaryEmbedding2") List cosineDistance, + @JsonProperty("summaryEmbedding3") List dotProduct, + @JsonProperty("rating") double rating) { + this.id = id; + this.name = name; + this.code = code; + this.description = description; + this.euclidean = euclidean; + this.cosineDistance = euclidean; + this.dotProduct = euclidean; + this.rating = rating; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + public List getEuclidean() { + return euclidean; + } + + public List getCosineDistance() { + return cosineDistance; + } + + public List getDotProduct() { + return dotProduct; + } + + public double getRating() { + return rating; + } + + public void setRating(double rating) { + this.rating = rating; + } +} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisHashSetVectorStoreRecordCollectionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisHashSetVectorStoreRecordCollectionTest.java new file mode 100644 index 000000000..6bfebb8d7 --- /dev/null +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisHashSetVectorStoreRecordCollectionTest.java @@ -0,0 +1,471 @@ +package com.microsoft.semantickernel.tests.data.redis; + +import com.microsoft.semantickernel.data.redis.RedisHashSetVectorStoreRecordCollection; +import com.microsoft.semantickernel.data.redis.RedisHashSetVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.redis.testcontainers.RedisContainer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import redis.clients.jedis.JedisPooled; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Testcontainers +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class RedisHashSetVectorStoreRecordCollectionTest { + + @Container private static final RedisContainer redisContainer = new RedisContainer("redis/redis-stack:latest"); + + private static final Map> optionsMap = new HashMap<>(); + + public enum RecordCollectionOptions { + DEFAULT, WITH_CUSTOM_DEFINITION + } + + @BeforeAll + static void setup() { + optionsMap.put(RecordCollectionOptions.DEFAULT, RedisHashSetVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + + List fields = new ArrayList<>(); + fields.add(VectorStoreRecordKeyField.builder() + .withName("id") + .withFieldType(String.class) + .build()); + fields.add(VectorStoreRecordDataField.builder() + .withName("name") + .withFieldType(String.class) + .build()); + fields.add(VectorStoreRecordDataField.builder() + .withName("code") + .withFieldType(Integer.class) + .build()); + fields.add(VectorStoreRecordDataField.builder() + .withName("description") + .withStorageName("summary") + .withFieldType(String.class) + .build()); + fields.add(VectorStoreRecordVectorField.builder() + .withName("euclidean") + .withStorageName("summaryEmbedding1") + .withFieldType(List.class) + .withDimensions(8) + .build()); + fields.add(VectorStoreRecordVectorField.builder() + .withName("cosineDistance") + .withStorageName("summaryEmbedding2") + .withFieldType(List.class) + .withDimensions(8) + .build()); + fields.add(VectorStoreRecordVectorField.builder() + .withName("dotProduct") + .withStorageName("summaryEmbedding3") + .withFieldType(List.class) + .withDimensions(8) + .build()); + fields.add(VectorStoreRecordDataField.builder() + .withName("rating") + .withFieldType(Double.class) + .isFilterable(true) + .build()); + VectorStoreRecordDefinition recordDefinition = VectorStoreRecordDefinition.fromFields(fields); + + optionsMap.put(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, RedisHashSetVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .withRecordDefinition(recordDefinition) + .build()); + + // Search configuration + List hotels = getHotels(); + + for (RecordCollectionOptions options : RecordCollectionOptions.values()) { + String collectionName = getCollectionName("search", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + recordCollection.createCollectionAsync().block(); + assertEquals(true, recordCollection.collectionExistsAsync().block()); + + recordCollection.upsertBatchAsync(hotels, null).block(); + } + + // Wait for data to be indexed + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private static RedisHashSetVectorStoreRecordCollection createCollection(@Nonnull RedisHashSetVectorStoreRecordCollectionOptions options, @Nonnull String collectionName) { + return new RedisHashSetVectorStoreRecordCollection<>(new JedisPooled(redisContainer.getRedisURI()), collectionName, RedisHashSetVectorStoreRecordCollectionOptions.builder() + .withRecordClass(options.getRecordClass()) + .withVectorStoreRecordMapper(options.getVectorStoreRecordMapper()) + .withRecordDefinition(options.getRecordDefinition()) + .withPrefixCollectionName(options.isPrefixCollectionName()) + .build()); + } + + private static List getHotels() { + return Arrays.asList( + new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", Arrays.asList(0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f),null, null, 4.0), + new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", Arrays.asList(-2.0f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f),null, null, 4.0), + new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.3f),null, null, 5.0), + new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", Arrays.asList(7.0f, 1.2f, -5.3f, 2.5f, 6.6f, -7.8f, 3.9f, -0.1f),null, null, 4.0), + new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", Arrays.asList(-3.5f, 4.4f, -1.2f, 9.9f, 5.7f, -6.1f, 7.8f, -2.0f),null, null, 4.0) + ); + } + + /** + * Search embeddings similar to the third hotel embeddings. + * In order of similarity: + * 1. Hotel 3 + * 2. Hotel 1 + * 3. Hotel 4 + */ + private static final List SEARCH_EMBEDDINGS = Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.2f); + + private static String getCollectionName(String id, RecordCollectionOptions options) { + return id + options.name(); + } + + @Order(1) + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void createCollectionAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("createCollectionAsync", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + assertEquals(false, recordCollection.collectionExistsAsync().block()); + recordCollection.createCollectionAsync().block(); + assertEquals(true, recordCollection.collectionExistsAsync().block()); + } + + @Test + public void deleteCollectionAsync() { + String collectionName = getCollectionName("deleteCollectionAsync", RecordCollectionOptions.DEFAULT); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(RecordCollectionOptions.DEFAULT), collectionName); + + assertEquals(false, recordCollection.collectionExistsAsync().block()); + recordCollection.createCollectionAsync().block(); + recordCollection.deleteCollectionAsync().block(); + assertEquals(false, recordCollection.collectionExistsAsync().block()); + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void upsertAndGetRecordAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("upsertAndGetRecordAsync", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + for (Hotel hotel : hotels) { + recordCollection.upsertAsync(hotel, null).block(); + } + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNotNull(retrievedHotel); + assertEquals(hotel.getId(), retrievedHotel.getId()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getBatchAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("getBatchAsync", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + for (Hotel hotel : hotels) { + recordCollection.upsertAsync(hotel, null).block(); + } + + List ids = new ArrayList<>(); + hotels.forEach(hotel -> ids.add(hotel.getId())); + + List retrievedHotels = recordCollection.getBatchAsync(ids, new GetRecordOptions(true)).block(); + + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + for (int i = 0; i < hotels.size(); i++) { + assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void upsertBatchAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("upsertBatchAsync", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + List keys = recordCollection.upsertBatchAsync(hotels, null).block(); + assertNotNull(keys); + + List retrievedHotels = (List) recordCollection.getBatchAsync(keys, null).block(); + + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + for (int i = 0; i < hotels.size(); i++) { + assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void deleteAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("deleteAsync", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + for (Hotel hotel : hotels) { + recordCollection.deleteAsync(hotel.getId(), null).block(); + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNull(retrievedHotel); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void deleteBatchAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("deleteBatchAsync", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List ids = new ArrayList<>(); + hotels.forEach(hotel -> ids.add(hotel.getId())); + + recordCollection.deleteBatchAsync(ids, null).block(); + + for (String id : ids) { + Hotel retrievedHotel = recordCollection.getAsync(id, null).block(); + assertNull(retrievedHotel); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getAsyncWithVectors(RecordCollectionOptions options) { + String collectionName = getCollectionName("getAsyncWithVectors", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), new GetRecordOptions(true)).block(); + assertNotNull(retrievedHotel); + assertNotNull(retrievedHotel.getEuclidean()); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertEquals(hotel.getDescription(), retrievedHotel.getDescription()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getBatchAsyncWithVectors(RecordCollectionOptions options) { + String collectionName = getCollectionName("getBatchAsyncWithVectors", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List ids = new ArrayList<>(); + hotels.forEach(hotel -> ids.add(hotel.getId())); + + List retrievedHotels = recordCollection.getBatchAsync(ids, new GetRecordOptions(true)).block(); + + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + for (int i = 0; i < hotels.size(); i++) { + assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); + assertEquals(hotels.get(i).getDescription(), retrievedHotels.get(i).getDescription()); + assertNotNull(retrievedHotels.get(i).getEuclidean()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getAsyncWithNoVectors(RecordCollectionOptions options) { + String collectionName = getCollectionName("getAsyncWithNoVectors", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + GetRecordOptions getRecordOptions = GetRecordOptions.builder().includeVectors(false).build(); + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), getRecordOptions).block(); + assertNotNull(retrievedHotel); + assertNull(retrievedHotel.getEuclidean()); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertEquals(hotel.getDescription(), retrievedHotel.getDescription()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getBatchAsyncWithNoVectors(RecordCollectionOptions options) { + String collectionName = getCollectionName("getBatchAsyncWithNoVectors", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + GetRecordOptions getRecordOptions = GetRecordOptions.builder().includeVectors(false).build(); + List ids = new ArrayList<>(); + hotels.forEach(hotel -> ids.add(hotel.getId())); + + List retrievedHotels = recordCollection.getBatchAsync(ids, getRecordOptions).block(); + + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + for (int i = 0; i < hotels.size(); i++) { + assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); + assertEquals(hotels.get(i).getDescription(), retrievedHotels.get(i).getDescription()); + assertNull(retrievedHotels.get(i).getEuclidean()); + } + } + + private static Stream provideSearchParameters() { + return Stream.of( + Arguments.of(RecordCollectionOptions.DEFAULT, "euclidean"), + Arguments.of(RecordCollectionOptions.DEFAULT, "cosineDistance"), + Arguments.of(RecordCollectionOptions.DEFAULT, "dotProduct"), + Arguments.of(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, "euclidean"), + Arguments.of(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, "cosineDistance"), + Arguments.of(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, "dotProduct") + ); + } + + private final String indexingFailureMessage = "If you are running in a slow machine, data might not be indexed yet. Adjust setup delay if needed"; + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void search(RecordCollectionOptions options, String embeddingName) { + String collectionName = getCollectionName("search", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions searchOptions = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, searchOptions).block().getResults(); + assertNotNull(results); + assertEquals(VectorSearchOptions.DEFAULT_TOP, results.size(), indexingFailureMessage); + // The third hotel should be the most similar + assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId(), indexingFailureMessage); + // Score should be different than zero + assertNotEquals(0.0, results.get(0).getScore()); + assertNull(results.get(0).getRecord().getEuclidean()); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void searchWithVectors(RecordCollectionOptions options, String embeddingName) { + String collectionName = getCollectionName("search", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions searchOptions = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withIncludeVectors(true) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, searchOptions).block().getResults(); + assertNotNull(results); + assertEquals(VectorSearchOptions.DEFAULT_TOP, results.size(), indexingFailureMessage); + // The third hotel should be the most similar + assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId(), indexingFailureMessage); + assertNotNull(results.get(0).getRecord().getEuclidean()); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void searchWithOffSet(RecordCollectionOptions options, String embeddingName) { + String collectionName = getCollectionName("search", options); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions searchOptions = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withSkip(1) + .withTop(4) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, searchOptions).block().getResults(); + assertNotNull(results); + assertEquals(4, results.size(), indexingFailureMessage); + // The first hotel should be the most similar + assertEquals(hotels.get(0).getId(), results.get(0).getRecord().getId(), indexingFailureMessage); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void searchWithFilterEqualToFilter(RecordCollectionOptions recordCollectionOptions, String embeddingName) { + String collectionName = getCollectionName("search", recordCollectionOptions); + RedisHashSetVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(recordCollectionOptions), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withTop(3) + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("rating", 4.0).build()) + .build(); + + // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, + options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The first hotel should be the most similar + assertEquals("id_1", results.get(0).getRecord().getId()); + } +} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisJsonVectorStoreRecordCollectionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisJsonVectorStoreRecordCollectionTest.java new file mode 100644 index 000000000..1190e4377 --- /dev/null +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisJsonVectorStoreRecordCollectionTest.java @@ -0,0 +1,471 @@ +package com.microsoft.semantickernel.tests.data.redis; + +import com.microsoft.semantickernel.data.redis.RedisJsonVectorStoreRecordCollection; +import com.microsoft.semantickernel.data.redis.RedisJsonVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.redis.testcontainers.RedisContainer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import redis.clients.jedis.JedisPooled; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Testcontainers +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class RedisJsonVectorStoreRecordCollectionTest { + + @Container private static final RedisContainer redisContainer = new RedisContainer("redis/redis-stack:latest"); + + private static final Map> optionsMap = new HashMap<>(); + + public enum RecordCollectionOptions { + DEFAULT, WITH_CUSTOM_DEFINITION + } + + @BeforeAll + static void setup() { + optionsMap.put(RecordCollectionOptions.DEFAULT, RedisJsonVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + + List fields = new ArrayList<>(); + fields.add(VectorStoreRecordKeyField.builder() + .withName("id") + .withFieldType(String.class) + .build()); + fields.add(VectorStoreRecordDataField.builder() + .withName("name") + .withFieldType(String.class) + .build()); + fields.add(VectorStoreRecordDataField.builder() + .withName("code") + .withFieldType(Integer.class) + .build()); + fields.add(VectorStoreRecordDataField.builder() + .withName("description") + .withStorageName("summary") + .withFieldType(String.class) + .build()); + fields.add(VectorStoreRecordVectorField.builder() + .withName("euclidean") + .withStorageName("summaryEmbedding1") + .withFieldType(List.class) + .withDimensions(8) + .build()); + fields.add(VectorStoreRecordVectorField.builder() + .withName("cosineDistance") + .withStorageName("summaryEmbedding2") + .withFieldType(List.class) + .withDimensions(8) + .build()); + fields.add(VectorStoreRecordVectorField.builder() + .withName("dotProduct") + .withStorageName("summaryEmbedding3") + .withFieldType(List.class) + .withDimensions(8) + .build()); + fields.add(VectorStoreRecordDataField.builder() + .withName("rating") + .withFieldType(Double.class) + .isFilterable(true) + .build()); + VectorStoreRecordDefinition recordDefinition = VectorStoreRecordDefinition.fromFields(fields); + + optionsMap.put(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, RedisJsonVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .withRecordDefinition(recordDefinition) + .build()); + + // Search configuration + List hotels = getHotels(); + + for (RecordCollectionOptions options : RecordCollectionOptions.values()) { + String collectionName = getCollectionName("search", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + recordCollection.createCollectionAsync().block(); + assertEquals(true, recordCollection.collectionExistsAsync().block()); + + recordCollection.upsertBatchAsync(hotels, null).block(); + } + + // Wait for data to be indexed + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private static RedisJsonVectorStoreRecordCollection createCollection(@Nonnull RedisJsonVectorStoreRecordCollectionOptions options, @Nonnull String collectionName) { + return new RedisJsonVectorStoreRecordCollection<>(new JedisPooled(redisContainer.getRedisURI()), collectionName, RedisJsonVectorStoreRecordCollectionOptions.builder() + .withRecordClass(options.getRecordClass()) + .withVectorStoreRecordMapper(options.getVectorStoreRecordMapper()) + .withRecordDefinition(options.getRecordDefinition()) + .withPrefixCollectionName(options.isPrefixCollectionName()) + .build()); + } + + private static List getHotels() { + return Arrays.asList( + new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", Arrays.asList(0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f),null, null, 4.0), + new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", Arrays.asList(-2.0f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f),null, null, 4.0), + new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.3f),null, null, 5.0), + new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", Arrays.asList(7.0f, 1.2f, -5.3f, 2.5f, 6.6f, -7.8f, 3.9f, -0.1f),null, null, 4.0), + new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", Arrays.asList(-3.5f, 4.4f, -1.2f, 9.9f, 5.7f, -6.1f, 7.8f, -2.0f),null, null, 4.0) + ); + } + + /** + * Search embeddings similar to the third hotel embeddings. + * In order of similarity: + * 1. Hotel 3 + * 2. Hotel 1 + * 3. Hotel 4 + */ + private static final List SEARCH_EMBEDDINGS = Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.2f); + + private static String getCollectionName(String id, RecordCollectionOptions options) { + return id + options.name(); + } + + @Order(1) + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void createCollectionAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("createCollectionAsync", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + assertEquals(false, recordCollection.collectionExistsAsync().block()); + recordCollection.createCollectionAsync().block(); + assertEquals(true, recordCollection.collectionExistsAsync().block()); + } + + @Test + public void deleteCollectionAsync() { + String collectionName = getCollectionName("deleteCollectionAsync", RecordCollectionOptions.DEFAULT); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(RecordCollectionOptions.DEFAULT), collectionName); + + assertEquals(false, recordCollection.collectionExistsAsync().block()); + recordCollection.createCollectionAsync().block(); + recordCollection.deleteCollectionAsync().block(); + assertEquals(false, recordCollection.collectionExistsAsync().block()); + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void upsertAndGetRecordAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("upsertAndGetRecordAsync", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + for (Hotel hotel : hotels) { + recordCollection.upsertAsync(hotel, null).block(); + } + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNotNull(retrievedHotel); + assertEquals(hotel.getId(), retrievedHotel.getId()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getBatchAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("getBatchAsync", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + for (Hotel hotel : hotels) { + recordCollection.upsertAsync(hotel, null).block(); + } + + List ids = new ArrayList<>(); + hotels.forEach(hotel -> ids.add(hotel.getId())); + + List retrievedHotels = recordCollection.getBatchAsync(ids, new GetRecordOptions(true)).block(); + + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + for (int i = 0; i < hotels.size(); i++) { + assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void upsertBatchAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("upsertBatchAsync", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + List keys = recordCollection.upsertBatchAsync(hotels, null).block(); + assertNotNull(keys); + + List retrievedHotels = (List) recordCollection.getBatchAsync(keys, null).block(); + + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + for (int i = 0; i < hotels.size(); i++) { + assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void deleteAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("deleteAsync", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + for (Hotel hotel : hotels) { + recordCollection.deleteAsync(hotel.getId(), null).block(); + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNull(retrievedHotel); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void deleteBatchAsync(RecordCollectionOptions options) { + String collectionName = getCollectionName("deleteBatchAsync", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List ids = new ArrayList<>(); + hotels.forEach(hotel -> ids.add(hotel.getId())); + + recordCollection.deleteBatchAsync(ids, null).block(); + + for (String id : ids) { + Hotel retrievedHotel = recordCollection.getAsync(id, null).block(); + assertNull(retrievedHotel); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getAsyncWithVectors(RecordCollectionOptions options) { + String collectionName = getCollectionName("getAsyncWithVectors", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), new GetRecordOptions(true)).block(); + assertNotNull(retrievedHotel); + assertNotNull(retrievedHotel.getEuclidean()); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertEquals(hotel.getDescription(), retrievedHotel.getDescription()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getBatchAsyncWithVectors(RecordCollectionOptions options) { + String collectionName = getCollectionName("getBatchAsyncWithVectors", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List ids = new ArrayList<>(); + hotels.forEach(hotel -> ids.add(hotel.getId())); + + List retrievedHotels = recordCollection.getBatchAsync(ids, new GetRecordOptions(true)).block(); + + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + for (int i = 0; i < hotels.size(); i++) { + assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); + assertEquals(hotels.get(i).getDescription(), retrievedHotels.get(i).getDescription()); + assertNotNull(retrievedHotels.get(i).getEuclidean()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getAsyncWithNoVectors(RecordCollectionOptions options) { + String collectionName = getCollectionName("getAsyncWithNoVectors", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + GetRecordOptions getRecordOptions = GetRecordOptions.builder().includeVectors(false).build(); + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), getRecordOptions).block(); + assertNotNull(retrievedHotel); + assertNull(retrievedHotel.getEuclidean()); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertEquals(hotel.getDescription(), retrievedHotel.getDescription()); + } + } + + @ParameterizedTest + @EnumSource(RecordCollectionOptions.class) + public void getBatchAsyncWithNoVectors(RecordCollectionOptions options) { + String collectionName = getCollectionName("getBatchAsyncWithNoVectors", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + GetRecordOptions getRecordOptions = GetRecordOptions.builder().includeVectors(false).build(); + List ids = new ArrayList<>(); + hotels.forEach(hotel -> ids.add(hotel.getId())); + + List retrievedHotels = recordCollection.getBatchAsync(ids, getRecordOptions).block(); + + assertNotNull(retrievedHotels); + assertEquals(hotels.size(), retrievedHotels.size()); + for (int i = 0; i < hotels.size(); i++) { + assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId()); + assertEquals(hotels.get(i).getDescription(), retrievedHotels.get(i).getDescription()); + assertNull(retrievedHotels.get(i).getEuclidean()); + } + } + + private static Stream provideSearchParameters() { + return Stream.of( + Arguments.of(RecordCollectionOptions.DEFAULT, "euclidean"), + Arguments.of(RecordCollectionOptions.DEFAULT, "cosineDistance"), + Arguments.of(RecordCollectionOptions.DEFAULT, "dotProduct"), + Arguments.of(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, "euclidean"), + Arguments.of(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, "cosineDistance"), + Arguments.of(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, "dotProduct") + ); + } + + private final String indexingFailureMessage = "If you are running in a slow machine, data might not be indexed yet. Adjust setup delay if needed"; + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void search(RecordCollectionOptions options, String embeddingName) { + String collectionName = getCollectionName("search", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions searchOptions = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, searchOptions).block().getResults(); + assertNotNull(results); + assertEquals(VectorSearchOptions.DEFAULT_TOP, results.size(), indexingFailureMessage); + // The third hotel should be the most similar + assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId(), indexingFailureMessage); + // Score should be different than zero + assertNotEquals(0.0, results.get(0).getScore()); + assertNull(results.get(0).getRecord().getEuclidean()); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void searchWithVectors(RecordCollectionOptions options, String embeddingName) { + String collectionName = getCollectionName("search", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions searchOptions = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withIncludeVectors(true) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, searchOptions).block().getResults(); + assertNotNull(results); + assertEquals(VectorSearchOptions.DEFAULT_TOP, results.size(), indexingFailureMessage); + // The third hotel should be the most similar + assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId(), indexingFailureMessage); + assertNotNull(results.get(0).getRecord().getEuclidean()); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void searchWithOffSet(RecordCollectionOptions options, String embeddingName) { + String collectionName = getCollectionName("search", options); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(options), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions searchOptions = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withSkip(1) + .withTop(4) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, searchOptions).block().getResults(); + assertNotNull(results); + assertEquals(4, results.size(), indexingFailureMessage); + // The first hotel should be the most similar + assertEquals(hotels.get(0).getId(), results.get(0).getRecord().getId(), indexingFailureMessage); + } + + @ParameterizedTest + @MethodSource("provideSearchParameters") + public void searchWithFilterEqualToFilter(RecordCollectionOptions recordCollectionOptions, String embeddingName) { + String collectionName = getCollectionName("search", recordCollectionOptions); + RedisJsonVectorStoreRecordCollection recordCollection = createCollection(optionsMap.get(recordCollectionOptions), collectionName); + + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(embeddingName) + .withTop(3) + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("rating", 4.0).build()) + .build(); + + // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned + List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, + options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The first hotel should be the most similar + assertEquals("id_1", results.get(0).getRecord().getId()); + } +} diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisVectorStoreTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisVectorStoreTest.java new file mode 100644 index 000000000..93ee1c205 --- /dev/null +++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/RedisVectorStoreTest.java @@ -0,0 +1,70 @@ +package com.microsoft.semantickernel.tests.data.redis; + +import com.microsoft.semantickernel.data.redis.RedisHashSetVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.redis.RedisJsonVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.redis.RedisStorageType; +import com.microsoft.semantickernel.data.redis.RedisVectorStore; +import com.microsoft.semantickernel.data.redis.RedisVectorStoreOptions; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.redis.testcontainers.RedisContainer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import redis.clients.jedis.JedisPooled; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +public class RedisVectorStoreTest { + @Container + private static final RedisContainer redisJsonContainer = new RedisContainer("redis/redis-stack:latest"); + @Container + private static final RedisContainer redisHashSetContainer = new RedisContainer("redis/redis-stack:latest"); + + public static JedisPooled buildClient(RedisStorageType storageType) { + if (storageType == RedisStorageType.JSON) { + return new JedisPooled(redisJsonContainer.getRedisURI()); + } else { + return new JedisPooled(redisHashSetContainer.getRedisURI()); + } + } + + private static VectorStoreRecordCollectionOptions getRecordCollectionOptions(RedisStorageType storageType) { + if (storageType == RedisStorageType.JSON) { + return RedisJsonVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build(); + } else { + return RedisHashSetVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build(); + } + } + + @ParameterizedTest + @EnumSource(RedisStorageType.class) + public void getCollectionNamesAsync(RedisStorageType storageType) { + RedisVectorStore vectorStore = new RedisVectorStore(buildClient(storageType), RedisVectorStoreOptions.builder() + .withStorageType(storageType) + .build()); + + List collectionNames = Arrays.asList("collection1", "collection2", "collection3"); + + for (String collectionName : collectionNames) { + vectorStore.getCollection(collectionName, getRecordCollectionOptions(storageType)).createCollectionAsync().block(); + } + + List retrievedCollectionNames = vectorStore.getCollectionNamesAsync().block(); + assertNotNull(retrievedCollectionNames); + assertEquals(collectionNames.size(), retrievedCollectionNames.size()); + for (String collectionName : collectionNames) { + assertTrue(retrievedCollectionNames.contains(collectionName)); + } + } +} diff --git a/api-test/integration-tests/src/test/resources/responseSchema.prompt.yaml b/api-test/integration-tests/src/test/resources/responseSchema.prompt.yaml new file mode 100644 index 000000000..08fa066a8 --- /dev/null +++ b/api-test/integration-tests/src/test/resources/responseSchema.prompt.yaml @@ -0,0 +1,27 @@ +name: testResponseSchema +description: Test Response Schema +template: | + Do something +template_format: handlebars +execution_settings: + default: + max_tokens: 10 + temperature: 0.2 + response_format: + type: json_schema + json_schema: + name: Test + strict: true + schema: | + { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + } + }, + "required" : [ + "name" + ], + "additionalProperties" : false + } diff --git a/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo-2_chat_completions-12b6744e-443f-4fe7-82e2-55cc41195ff1.json b/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo-2_chat_completions-12b6744e-443f-4fe7-82e2-55cc41195ff1.json index 24f15f34d..4b27e3ba0 100644 --- a/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo-2_chat_completions-12b6744e-443f-4fe7-82e2-55cc41195ff1.json +++ b/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo-2_chat_completions-12b6744e-443f-4fe7-82e2-55cc41195ff1.json @@ -2,7 +2,7 @@ "priority": 1, "request": { "method": "POST", - "url": "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2024-03-01-preview", + "url": "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2025-01-01-preview", "bodyPatterns": [ { "contains": "That is all" diff --git a/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo-2_chat_completions-71b07779-49a8-44e5-a60b-ee5b0a3ad697.json b/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo-2_chat_completions-71b07779-49a8-44e5-a60b-ee5b0a3ad697.json index 2b2e290bb..208f334c1 100644 --- a/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo-2_chat_completions-71b07779-49a8-44e5-a60b-ee5b0a3ad697.json +++ b/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo-2_chat_completions-71b07779-49a8-44e5-a60b-ee5b0a3ad697.json @@ -1,7 +1,7 @@ { "request": { "method": "POST", - "url": "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2024-03-01-preview" + "url": "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2025-01-01-preview" }, "response": { "body": "{\"id\":\"chatcmpl-xxx\",\"object\":\"chat.completion\",\"created\":1707253061,\"model\":\"gpt-35-turbo\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"1979b4ce-5463-4cfb-8ec8-1d05c4b44ccf\"},\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"usage\":{\"prompt_tokens\":17,\"completion_tokens\":67,\"total_tokens\":84}}", diff --git a/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo_chat_completions-0c8046c5-74ad-4836-8aa9-09da60f367a2.json b/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo_chat_completions-0c8046c5-74ad-4836-8aa9-09da60f367a2.json index c909297f8..766450c6e 100644 --- a/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo_chat_completions-0c8046c5-74ad-4836-8aa9-09da60f367a2.json +++ b/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_gpt-35-turbo_chat_completions-0c8046c5-74ad-4836-8aa9-09da60f367a2.json @@ -1,7 +1,7 @@ { "request": { "method": "POST", - "url": "//openai/deployments/gpt-35-turbo/chat/completions?api-version=2024-03-01-preview" + "url": "//openai/deployments/gpt-35-turbo/chat/completions?api-version=2025-01-01-preview" }, "response": { "body": "{\"id\":\"chatcmpl-xxx\",\"object\":\"chat.completion\",\"created\":1707253039,\"model\":\"gpt-35-turbo\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"ac9817bc-7e1a-48e4-b06c-0ff7618b88c6\"},\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"usage\":{\"prompt_tokens\":26,\"completion_tokens\":131,\"total_tokens\":157}}", diff --git a/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_text-davinci-003_completions-0215b128-4822-4368-ac3d-2f580a221f00.json b/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_text-davinci-003_completions-0215b128-4822-4368-ac3d-2f580a221f00.json index 3a0ea31ce..56bccb38d 100644 --- a/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_text-davinci-003_completions-0215b128-4822-4368-ac3d-2f580a221f00.json +++ b/api-test/integration-tests/src/test/resources/wiremock/mappings/deployments_text-davinci-003_completions-0215b128-4822-4368-ac3d-2f580a221f00.json @@ -1,7 +1,7 @@ { "request": { "method": "POST", - "url": "//openai/deployments/text-davinci-003/completions?api-version=2024-03-01-preview" + "url": "//openai/deployments/text-davinci-003/completions?api-version=2025-01-01-preview" }, "response": { "body": "{\"id\":\"cmpl-xxx\",\"object\":\"text_completion\",\"created\":1707253062,\"model\":\"text-davinci-003\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"choices\":[{\"text\":\"7949e981-c632-422f-9b76-335a2379cd83\",\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":26,\"total_tokens\":36}}", diff --git a/api-test/pom.xml b/api-test/pom.xml index 7effdac35..7b023c30a 100644 --- a/api-test/pom.xml +++ b/api-test/pom.xml @@ -6,7 +6,7 @@ com.microsoft.semantic-kernel semantickernel-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -34,6 +34,28 @@ + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-aiservices-openai + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + @@ -57,6 +79,7 @@ com.diffplug.spotless spotless-maven-plugin + ${maven.spotless-plugin.version} true diff --git a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/JsonMemoryRecord.java b/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/JsonMemoryRecord.java deleted file mode 100644 index 68ca9d400..000000000 --- a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/JsonMemoryRecord.java +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.memory.redis; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.microsoft.semantickernel.ai.embeddings.Embedding; -import com.microsoft.semantickernel.memory.MemoryRecord; -import com.microsoft.semantickernel.memory.MemoryRecordMetadata; -import java.nio.charset.StandardCharsets; -import java.util.*; - -/** Represents an entry in the Semantic Kernel Memory Table. */ -public class JsonMemoryRecord { - - static final String ID = "Id"; - static final String TEXT = "Text"; - static final String DESCRIPTION = "Description"; - static final String ADDITIONAL_METADATA = "AdditionalMetadata"; - static final String EMBEDDING = "Embedding"; - static final String EXTERNAL_SOURCE_NAME = "ExternalSourceName"; - static final String IS_REFERENCE = "Reference"; - static final String TIME = "Timestamp"; - - private final String id; - private final String text; - private final String description; - private final String additionalMetadata; - private final List embedding; - private final String externalSourceName; - private final boolean isReference; - - @JsonCreator - public JsonMemoryRecord( - @JsonProperty(ID) String id, - @JsonProperty(TEXT) String text, - @JsonProperty(DESCRIPTION) String description, - @JsonProperty(ADDITIONAL_METADATA) String additionalMetadata, - @JsonProperty(EMBEDDING) List embedding, - @JsonProperty(EXTERNAL_SOURCE_NAME) String externalSourceName, - @JsonProperty(IS_REFERENCE) boolean isReference) { - this.id = id; - this.text = text; - this.description = description; - this.additionalMetadata = additionalMetadata; - this.embedding = embedding != null ? embedding : Collections.emptyList(); - 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; - } - - /** - * Embedding vector. - * - * @return Embedding vector. - */ - public List getEmbedding() { - return Collections.unmodifiableList(embedding); - } - - /** - * 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; - } - - public MemoryRecordMetadata toMemoryRecordMetadata() { - return new MemoryRecordMetadata( - this.isReference, - decodeId(this.id), - this.text, - this.description, - this.externalSourceName, - this.additionalMetadata); - } - - public static JsonMemoryRecord fromMemoryRecord(MemoryRecord record) { - return new JsonMemoryRecord( - record.getMetadata().getId(), - record.getMetadata().getText(), - record.getMetadata().getDescription(), - record.getMetadata().getAdditionalMetadata(), - record.getEmbedding().getVector(), - record.getMetadata().getExternalSourceName(), - record.getMetadata().isReference()); - } - - public MemoryRecord toMemoryRecord() { - return toMemoryRecord(true); - } - - public MemoryRecord toMemoryRecord(boolean withEmbeddings) { - return new MemoryRecord( - this.toMemoryRecordMetadata(), - new Embedding(withEmbeddings ? this.embedding : Collections.emptyList()), - this.id, - null); - } - - // ACS keys can contain only letters, digits, underscore, dash, equal sign, recommending - // to encode values with a URL-safe algorithm. - // Original Id - // Encoded id - static String encodeId(String realId) { - byte[] bytes = Base64.getUrlEncoder().encode(realId.getBytes(StandardCharsets.UTF_8)); - return new String(bytes, StandardCharsets.UTF_8); - } - - static String decodeId(String encodedId) { - byte[] bytes = Base64.getUrlDecoder().decode(encodedId.getBytes(StandardCharsets.UTF_8)); - return new String(bytes, StandardCharsets.UTF_8); - } -} diff --git a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisException.java b/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisException.java deleted file mode 100644 index 2060938a4..000000000 --- a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisException.java +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.memory.redis; - -import com.microsoft.semantickernel.SKException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** Exception thrown by the SQL connector. */ -public class RedisException extends SKException { - - private final ErrorCodes errorCode; - - /** - * Create an exception with a message - * - * @param errorCode The error code - */ - public RedisException(@Nonnull ErrorCodes errorCode) { - - this(errorCode, null, null); - } - - /** - * Create an exception with a message - * - * @param errorCode The error code - * @param message a description of the cause of the exception - */ - public RedisException(@Nonnull ErrorCodes errorCode, @Nullable String message) { - - this(errorCode, message, null); - } - - /** - * Create an exception with a message and a cause - * - * @param errorCode the error code - * @param message a description of the cause of the exception - * @param cause the cause of the exception - */ - public RedisException( - @Nonnull ErrorCodes errorCode, @Nullable String message, @Nullable Throwable cause) { - super(message, cause); - this.errorCode = errorCode; - } - - public ErrorCodes getErrorCode() { - return errorCode; - } - - public enum ErrorCodes { - REDIS_ERROR("Redis error"), - UNKNOWN_ERROR("Unknown error"), - INVALID_EMBEDDING_SIZE("Invalid embedding size"), - INVALID_INDEX_NAME("Invalid index name"), - READ_FAILURE("Read failure"), - WRITE_FAILURE("Write failure"); - - final String message; - - ErrorCodes(String message) { - this.message = message; - } - - public String getMessage() { - return message; - } - } -} diff --git a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisIndexSchemaParams.java b/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisIndexSchemaParams.java deleted file mode 100644 index e3f4eb9db..000000000 --- a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisIndexSchemaParams.java +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.memory.redis; - -public class RedisIndexSchemaParams { - - public static final String TYPE = "TYPE"; - public static final String DIM = "DIM"; - public static final String DIST = "DISTANCE_METRIC"; -} diff --git a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisMemoryRecord.java b/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisMemoryRecord.java deleted file mode 100644 index db6870456..000000000 --- a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisMemoryRecord.java +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.memory.redis; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.semantickernel.ai.embeddings.Embedding; -import com.microsoft.semantickernel.memory.MemoryRecord; -import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; -import redis.clients.jedis.search.Document; - -/** Represents an entry in the Semantic Kernel Memory Table. */ -public class RedisMemoryRecord extends JsonMemoryRecord { - - public RedisMemoryRecord( - String id, - String text, - String description, - String additionalMetadata, - List embedding, - String externalSourceName, - boolean isReference) { - super( - id, - text, - description, - additionalMetadata, - embedding, - externalSourceName, - isReference); - } - - /** - * Converts a RedisSearch Document to a HashMap. - * - * @param document A Redis Search Document - * @return A HashMap containing the iterator's entries - */ - protected Map documentToMap(Document document) { - return StreamSupport.stream(document.getProperties().spliterator(), false) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - /** - * Gets the embedding information associated with the entry. - * - * @param map A hashmap with keys representing a record - * @param withEmbedding A boolean flag indicating that the embedding should be returned with the - * metadata - * @return A MemoryRecord object. - */ - public static MemoryRecord mapToRecord(Map map, boolean withEmbedding) { - Embedding embedding = Embedding.empty(); - ZonedDateTime time = null; - ObjectMapper mapper = new ObjectMapper(); - try { - if (map.get(TIME) != null) { - time = - mapper.readValue( - map.getOrDefault(TIME, ZonedDateTime.now().toString()), - ZonedDateTime.class); - } - if (withEmbedding) { - List embeddings = mapper.readValue(map.get(EMBEDDING), List.class); - embedding = new Embedding(embeddings); - } - } catch (JsonProcessingException e) { - throw new RedisException( - RedisException.ErrorCodes.REDIS_ERROR, "Error deserializing Redis entry", e); - } - - return MemoryRecord.localRecord( - map.get(ID), - map.get(TEXT), - map.get(DESCRIPTION), - embedding, - map.get(ADDITIONAL_METADATA), - map.get(EXTERNAL_SOURCE_NAME), - time); - } - - /** - * Gets the embedding information associated with the entry. - * - * @param record A MemoryRecord object - * @return A hashmap containing the entries of the record. - */ - public static Map recordToMap(MemoryRecord record) { - - Map map = new HashMap(); - - map.put(ID, record.getMetadata().getId()); - map.put(EXTERNAL_SOURCE_NAME, record.getMetadata().getExternalSourceName()); - map.put(TEXT, record.getMetadata().getText()); - map.put(DESCRIPTION, record.getMetadata().getDescription()); - map.put(EMBEDDING, record.getEmbedding().getVector()); - map.put(ADDITIONAL_METADATA, record.getMetadata().getAdditionalMetadata()); - map.put(IS_REFERENCE, record.getMetadata().isReference()); - - return map; - } -} diff --git a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisMemoryStore.java b/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisMemoryStore.java deleted file mode 100644 index 6a40d3d57..000000000 --- a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisMemoryStore.java +++ /dev/null @@ -1,658 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.memory.redis; - -import com.microsoft.semantickernel.SKException; -import com.microsoft.semantickernel.ai.embeddings.Embedding; -import com.microsoft.semantickernel.memory.MemoryException; -import com.microsoft.semantickernel.memory.MemoryException.ErrorCodes; -import com.microsoft.semantickernel.memory.MemoryRecord; -import com.microsoft.semantickernel.memory.MemoryStore; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.text.MessageFormat; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; -import redis.clients.jedis.JedisPooled; -import redis.clients.jedis.exceptions.JedisDataException; -import redis.clients.jedis.resps.ScanResult; -import redis.clients.jedis.search.Document; -import redis.clients.jedis.search.IndexDefinition; -import redis.clients.jedis.search.IndexOptions; -import redis.clients.jedis.search.Query; -import redis.clients.jedis.search.Schema; -import redis.clients.jedis.search.SearchResult; -import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm; - -/** - * Semantic Memory implementation using Redis Vector Search. For more information about Redis Vector - * Search {@see https://redis.com/solutions/use-cases/vector-database/} - */ -public class RedisMemoryStore implements MemoryStore { - - private static final VectorAlgorithm DefaultIndexAlgorithm = - VectorAlgorithm.HNSW; // FLAT or HNSW - private static final String DefaultVectorType = - RedisVectorType.FLOAT32; // prefer better accuracy - private static final String DefaultDistanceMetric = RedisVectorDistanceMetric.L2; - private static final Integer DefaultQueryDialect = - 2; // this has to be >= 2 for vector functionality - private static final Integer DefaultVectorSize = 1536; // framework model dependent - ada-002 - private static final String SUFFIX = "sk"; - private static final String INDEX = "idx"; - private static final String OK = "OK"; - - private final JedisPooled client; - private final String vectorDistanceMetric; - - @SuppressFBWarnings("SS_SHOULD_BE_STATIC") - private final String vectorType = DefaultVectorType; - - private final Integer queryDialect; - - public static void isXinRange(Integer x, Integer lower, Integer upper, String message) { - if (lower > x || x > upper) { - throw new IllegalArgumentException(MessageFormat.format(message, x)); - } - } - - /** - * Create a new instance of semantic memory using Redis. - * - * @param client A Redis Database client connection. - * @param vectorSize Embedding vector size, defaults to 1536 - framework model dependent - - * ada-002. - * @param vectorDistanceMetric Metric for measuring vector distances, defaults to "COSINE" - * @param queryDialect Query dialect, must be 2 or greater for vector similarity searching, - * defaults to 2 - */ - @SuppressFBWarnings("EI_EXPOSE_REP2") - public RedisMemoryStore( - JedisPooled client, - Integer vectorSize, - String vectorDistanceMetric, - Integer queryDialect) { - - isXinRange( - vectorSize, - 0, - 8192, - "Invalid vector size: {x}. Vector size must be in the range 0-8192."); - isXinRange( - queryDialect, - 2, - 4, - "Invalid query dialect: {x}. Query dialect must be in the range 2-4."); - - this.client = client; - this.vectorDistanceMetric = vectorDistanceMetric; - this.queryDialect = queryDialect; - } - - /** - * Create a new instance of semantic memory using Redis. - * - * @param connectionString Provide connection URL to a Redis instance. - * @param vectorSize Embedding vector size, defaults to 1536. - * @param vectorIndexAlgorithm Indexing algorithm for vectors, defaults to "HNSW" - * @param vectorDistanceMetric Metric for measuring vector distances, defaults to "COSINE" - * @param queryDialect Query dialect, must be 2 or greater for vector similarity searching, - * defaults to 2 - */ - public RedisMemoryStore( - String connectionString, - Integer vectorSize, - VectorAlgorithm vectorIndexAlgorithm, - String vectorDistanceMetric, - Integer queryDialect) { - - isXinRange( - vectorSize, - 0, - 8192, - "Invalid vector size: {x}. Vector size must be in the range 0-8192."); - isXinRange( - queryDialect, - 2, - 4, - "Invalid query dialect: {x}. Query dialect must be in the range 2-4."); - - this.client = new JedisPooled(connectionString); - this.vectorDistanceMetric = vectorDistanceMetric; - this.queryDialect = queryDialect; - } - - /** - * Returns a Redis key - * - * @param collectionName The name associated with a collection of embeddings. - * @param key The unique id associated with the memory record to get. - * @return A Redis key that identifies a particular Hash of Json object. - */ - private static String getRedisKey(String collectionName, String key) { - if (key == null) { - throw new SKException("cannot create a key with a null value"); - } - - if (collectionName == null) { - throw new SKException("cannot form a redis key with a null collection name"); - } - - String name = collectionName.toLowerCase(Locale.ROOT); - String id = JsonMemoryRecord.encodeId(key); - return String.format("%s:%s", name, id); - } - - /** - * Returns a long timestamp given a OffsetDateTime - * - * @param timestamp An OffsetDateTime. - * @return A long timestamp or -1 in the case of an error. - */ - private static long toTimestampLong(OffsetDateTime timestamp) { - if (timestamp != null) { - return timestamp.toInstant().toEpochMilli(); - } - return -1; - } - - /** - * Returns an OffsetDateTime given a valid timestamp - * - * @param timestamp A long timestamp value. - * @return An OffsetDateTime if the timestamp is valid, else null. - */ - private static OffsetDateTime toDateTime(long timestamp) { - if (timestamp > 0) { - Instant instant = Instant.ofEpochMilli(timestamp); - return OffsetDateTime.ofInstant(instant, ZoneId.systemDefault()); - } - return null; - } - - @Override - public Mono> getCollectionsAsync() { - return getIndexesAsync() - .map( - names -> - names.stream() - .map( - name -> - name.substring( - 0, - name.length() - "-sk-idx".length())) - .collect(Collectors.toList())); - } - - /** - * Returns a List of Index(Collection) Names - * - * @return A list of names of the embedding indexes, including suffix, in the database - */ - private Mono> getIndexesAsync() { - return Mono.just( - this.client.ftList().stream() - .filter(name -> name.endsWith("-sk-idx")) - .collect(Collectors.toList())); - } - - @Override - public Mono createCollectionAsync(@Nonnull String collectionName) { - // Indexes are created when sending a record; creation requires the size of the embedding - // vector - return Mono.empty(); - } - - public Mono doesIndexExistAsync(@Nonnull String collectionName) { - - try { - Map info = this.client.ftInfo(collectionName); - return Mono.just(info != null && !info.isEmpty()); - } catch (Exception e) { - if (!(e instanceof JedisDataException)) { - throw e; - } - return Mono.just(false); - } - // return Mono.just(this.client.ftInfo(createIndexName(collectionName)) != null); - } - - @Override - public Mono doesCollectionExistAsync(@Nonnull String collectionName) { - - Objects.requireNonNull(collectionName); - String normalizedIndexName = createIndexName(collectionName); - return getIndexesAsync() - .map( - list -> - list.stream() - .anyMatch( - name -> - name.equalsIgnoreCase(collectionName) - || name.equalsIgnoreCase( - normalizedIndexName))); - } - - @Override - public Mono deleteCollectionAsync(@Nonnull String collectionName) { - Objects.requireNonNull(collectionName); - try { - this.client.ftDropIndex(createIndexName(collectionName)); - return Mono.empty(); - } catch (Exception e) { - return Mono.error(e); - } - } - - @Override - public Mono getAsync( - @Nonnull String collectionName, @Nonnull String key, boolean withEmbedding) { - - Map map = - this.client.hgetAll( - String.format("%s:%s", collectionName, key).toLowerCase(Locale.ROOT)); - if (map == null || map.isEmpty()) { - return Mono.empty(); - } - MemoryRecord record = RedisMemoryRecord.mapToRecord(map, withEmbedding); - return Mono.just(record); - } - - public Mono getInternalAsync( - @Nonnull String collectionName, @Nonnull String key, boolean withEmbedding) { - - Objects.requireNonNull(collectionName); - Objects.requireNonNull(key); - - Map entry = - this.client.hgetAll(String.format("%s:%s", collectionName, key)); - - if (entry == null) return Mono.empty(); - - return Mono.just(RedisMemoryRecord.mapToRecord(entry, withEmbedding)); - } - - /** - * Converts a RedisSearch Document to a HashMap. - * - * @param document A Redis Search Document - * @return A HashMap containing the iterator's entries - */ - protected Map documentToMap(Document document) { - - return iterableToMap(document.getProperties()); - } - - /** - * Converts an Iterable> to a HashMap. - * - * @param entryIterator An Iterable> - * @return A HashMap containing the iterator's entries - */ - protected Map iterableToMap(Iterable> entryIterator) { - - Map entryMap = new HashMap<>(); - entryIterator.forEach((e) -> entryMap.put(e.getKey(), e.getValue().toString())); - - return entryMap; - } - - protected MemoryRecord documentToMemoryRecord(Document document, boolean withEmbedding) { - - Map map = documentToMap(document); - return RedisMemoryRecord.mapToRecord(map, withEmbedding); - } - - @Override - public Mono> getBatchAsync( - @Nonnull String collectionName, - @Nonnull Collection keys, - boolean withEmbeddings) { - Objects.requireNonNull(collectionName); - // ACS issues one query per key; redis has several possible calls, fastest will be hkeys - // followed by hvals - - return Flux.fromIterable(keys) - .flatMap(key -> getAsync(collectionName, key, withEmbeddings)) - .collectList() - .map(records -> records); - } - - @Override - public Mono upsertAsync(@Nonnull String collectionName, @Nonnull MemoryRecord record) { - return upsertRecordAsync(collectionName, record); - } - - /** - * Returns the key of the upserted object (wraps the record in a list and calls the batch - * function). - * - * @param collectionName The name associated with a collection of embeddings. - * @param record The memory record to upsert. - * @return The key of the newly created record. - */ - public Mono upsertRecordAsync( - @Nonnull String collectionName, @Nonnull MemoryRecord record) { - return upsertBatchAsync(collectionName, Collections.singletonList(record)) - .map(Collection::iterator) - .map(Iterator::next); - } - - @Override - public Mono> upsertBatchAsync( - @Nonnull String collectionName, @Nonnull Collection records) { - - Objects.requireNonNull(collectionName); - Objects.requireNonNull(records); - - if (records.isEmpty()) { - return Mono.just(Collections.emptyList()); - } - - doesCollectionExistAsync(collectionName) - .map( - exists -> { - if (!exists) { - int embeddingSize = - records.stream() - .map(record -> record.getEmbedding().getVector()) - .map(List::size) - .max(Integer::compareTo) - .orElse(0); - createIndexAsync(collectionName, embeddingSize); - } - return true; - }); - - return Mono.just( - records.stream() - .map( - record -> { - Map map = RedisMemoryRecord.recordToMap(record); - // we have to change the embeddings to bytes - it's easiest to - // modify the map - List embedding = record.getEmbedding().getVector(); - map.replace( - RedisMemoryRecord.EMBEDDING, - embedding, - embeddingToBytes(embedding)); - String key = - getRedisKey( - collectionName, record.getMetadata().getId()); - this.client.hsetObject(key, map); - return key; - }) - .collect(Collectors.toList())); - } - - @Override - public Mono removeAsync(@Nonnull String collectionName, @Nonnull String key) { - return Mono.fromRunnable( - () -> this.client.hdel(String.format("%s:%s", collectionName, key))); - } - - @Override - public Mono removeBatchAsync( - @Nonnull String collectionName, @Nonnull Collection keys) { - return Mono.fromRunnable( - () -> { - // ScanResult results = this.client.scan(collectionName); - // results.getResult().forEach(key -> this.client.hdel(key)); - keys.forEach( - key -> this.client.hdel(String.format("%s:%s", collectionName, key))); - }); - } - - /** - * Create a new search index. - * - * @param collectionName Index name - * @param embeddingSize Size of the embedding vector - * @return A Mono that completes when the index is created - */ - private Mono createIndexAsync(@Nonnull String collectionName, int embeddingSize) { - if (embeddingSize < 1) { - throw new RedisException( - RedisException.ErrorCodes.INVALID_EMBEDDING_SIZE, - "the value must be greater than zero"); - } - - Map attributes = new HashMap<>(); - - attributes.put(RedisIndexSchemaParams.TYPE, vectorType); - attributes.put(RedisIndexSchemaParams.DIM, embeddingSize); - attributes.put(RedisIndexSchemaParams.DIST, vectorDistanceMetric); - - Schema schema = - new Schema() - .addVectorField("Embedding", Schema.VectorField.VectorAlgo.FLAT, attributes) - .as("Embedding") - .addTextField("Id", 1.0) - .as("Id") - .addTextField("Text", 1.0) - .as("Text") - .addTextField("Description", 1.0) - .as("Description") - .addTextField("AdditionalMetadata", 1.0) - .as("AdditionalMetadata") - .addTextField("ExternalSourceName", 1.0) - .as("ExternalSourceName"); - - IndexDefinition rule = - new IndexDefinition(IndexDefinition.Type.HASH) - .setPrefixes(collectionName.toLowerCase() + ":"); - // customarily redis indexes are named -idx; we're going to suffix them - // (-sk-idx) so we can find them - String normalizedIndexName = createIndexName(collectionName); - String result = - this.client.ftCreate( - normalizedIndexName, - IndexOptions.defaultOptions().setDefinition(rule), - schema); - - if (result.equals(OK)) { - return Mono.just(true); - } - return null; - } - - @Override - public Mono>> getNearestMatchesAsync( - @Nonnull String collectionName, - @Nonnull Embedding embedding, - int limit, - float minRelevanceScore, - boolean withEmbedding) { - - Objects.requireNonNull(collectionName); - Objects.requireNonNull(embedding); - - if (limit <= 0) { - return Mono.just(Collections.emptyList()); - } - Query query = - new Query("*=>[KNN $k @Embedding $vec AS vector_score]") - .returnFields("Id", "Text", "Description", "vector_score") - .setSortBy("vector_score", true) - .addParam("k", limit) - .addParam("vec", embeddingToBytes(embedding.getVector())) - .limit(0, limit) - .dialect(queryDialect); - String indexName = createIndexName(collectionName); - SearchResult results = this.client.ftSearch(indexName, query); - - // TODO convert to Collection> - Collection> searchTuples = - results.getDocuments().stream() - .map( - document -> { - MemoryRecord memoryRecord = - documentToMemoryRecord(document, withEmbedding); - return Tuples.of( - memoryRecord, document.getScore().floatValue()); - }) - .collect(Collectors.toList()); - return Mono.just(searchTuples); - } - - @Override - public Mono> getNearestMatchAsync( - @Nonnull String collectionName, - @Nonnull Embedding embedding, - float minRelevanceScore, - boolean withEmbedding) { - return getNearestMatchesAsync( - collectionName, embedding, 1, minRelevanceScore, withEmbedding) - .flatMap( - nearestMatches -> { - if (nearestMatches.isEmpty()) { - return Mono.empty(); - } - return Mono.just(nearestMatches.iterator().next()); - }); - } - - /** - * Convert the Azure List embedding structure to byte[] for storage in Redis - * - * @param embeddings the list of vectors returned by the model - * @return byte[] - */ - private byte[] embeddingToBytes(List embeddings) { - ByteBuffer bytes = ByteBuffer.allocate(Float.BYTES * embeddings.size()); - bytes.order(ByteOrder.LITTLE_ENDIAN); - embeddings.iterator().forEachRemaining(bytes::putFloat); - return bytes.array(); - } - - private static OffsetDateTime ParseTimestamp(Long timestamp) { - if (timestamp != null && timestamp > 0) { - return OffsetDateTime.parse(timestamp.toString()); - } - return null; - } - - /** - * Normalize an index name - * - * @param indexName Index name - * @return A string to be used as the root of the index name; minus '-idx' - */ - private static String normalizeCollectionName(String indexName) { - if (indexName.length() > 128) { - throw new IllegalArgumentException("The collection name cannot exceed 128 chars"); - } - return indexName.toLowerCase(Locale.ROOT); - } - - private static String createIndexName(String collectionName) { - - if (collectionName.length() > 128) { - throw new IllegalArgumentException("The indexName name cannot exceed 128 chars"); - } - return String.format( - "%s-%s-%s", - normalizeCollectionName(collectionName), RedisMemoryStore.SUFFIX, INDEX); - } - - protected Map getCollection(@Nonnull String collectionName) { - Objects.requireNonNull(collectionName); - ScanResult results = this.client.scan(collectionName); - - if (results == null) - throw new MemoryException( - ErrorCodes.ATTEMPTED_TO_ACCESS_NONEXISTENT_COLLECTION, collectionName); - - return Collections.unmodifiableMap( - (Map) - results.getResult().stream() - .map( - key -> - RedisMemoryRecord.mapToRecord( - this.client.hgetAll( - String.format( - "%s:%s", - collectionName, key)), - false))); - } - - public static class Builder implements MemoryStore.Builder { - private String connectionString; - private int vectorSize; // in theory determined by the model but can be overridden - private VectorAlgorithm vectorIndexAlgorithm; - private String vectorDistanceMetric; - private int queryDialect = 0; // must best at least two for vector index - - public Builder connectionString(String connectionString) { - this.connectionString = connectionString; - return this; - } - - public Builder vectorSize(int vectorSize) { - this.vectorSize = vectorSize; - return this; - } - - public Builder vectorIndexAlgorithm(VectorAlgorithm vectorIndexAlgorithm) { - this.vectorIndexAlgorithm = vectorIndexAlgorithm; - return this; - } - - public Builder vectorDistanceMetric(String vectorDistanceMetric) { - this.vectorDistanceMetric = vectorDistanceMetric; - return this; - } - - public Builder queryDialect(int queryDialect) { - this.queryDialect = queryDialect; - return this; - } - - @Override - /* - * Create a RedisMemoryStore using assigned values. - * - * @return A RedisMemoryStore object configured with the specified values - */ - public RedisMemoryStore build() { - - // make sure everything we need has been set - return new RedisMemoryStore( - connectionString, - vectorSize, - vectorIndexAlgorithm, - vectorDistanceMetric, - queryDialect); - } - - /** - * Create a RedisMemoryStore object using default settings; requires a valid connection - * string. - * - * @return A RedisMemoryStore object configured with default values - */ - public RedisMemoryStore buildDefault() { - return new RedisMemoryStore( - connectionString, - DefaultVectorSize, - RedisMemoryStore.DefaultIndexAlgorithm, - RedisMemoryStore.DefaultDistanceMetric, - RedisMemoryStore.DefaultQueryDialect); - } - } -} diff --git a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisVectorDistanceMetric.java b/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisVectorDistanceMetric.java deleted file mode 100644 index 06d176431..000000000 --- a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisVectorDistanceMetric.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.memory.redis; - -/* - Supported distance metrics are {L2, IP, COSINE}. The default value is "COSINE". - - */ -public class RedisVectorDistanceMetric { - - /* Euclidean distance between two vectors */ - public static final String L2 = "L2"; - - /* Inner product of two vectors */ - public static final String IP = "IP"; - - /* Cosine distance of two vectors */ - public static final String COS = "COSINE"; -} diff --git a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisVectorType.java b/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisVectorType.java deleted file mode 100644 index 920146816..000000000 --- a/connectors/semantickernel-connectors-memory-redis/src/main/java/com/microsoft/semantickernel/connectors/memory/redis/RedisVectorType.java +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.memory.redis; - -public class RedisVectorType { - public static final String FLOAT32 = "FLOAT32"; - public static final String FLOAT64 = "FLOAT64"; -} diff --git a/data/semantickernel-data-azureaisearch/pom.xml b/data/semantickernel-data-azureaisearch/pom.xml new file mode 100644 index 000000000..714c99484 --- /dev/null +++ b/data/semantickernel-data-azureaisearch/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../../pom.xml + + + semantickernel-data-azureaisearch + Semantic Kernel Azure AI Search connector + Azure AI Search connector for Semantic Kernel + + + + com.azure + azure-ai-openai + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-api-data + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + + com.azure + azure-search-documents + + + com.azure + azure-core-serializer-json-jackson + + + + + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + com.microsoft.semantic-kernel + semantickernel-api-builders + test + + + + \ No newline at end of file diff --git a/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStore.java b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStore.java new file mode 100644 index 000000000..66dbb3c6b --- /dev/null +++ b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStore.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.azureaisearch; + +import com.azure.search.documents.indexes.SearchIndexAsyncClient; +import com.azure.search.documents.indexes.models.SearchIndex; +import com.microsoft.semantickernel.data.vectorstorage.VectorStore; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import reactor.core.publisher.Mono; + +/** + * Represents an Azure AI Search vector store. + */ +public class AzureAISearchVectorStore implements VectorStore { + + private final SearchIndexAsyncClient searchIndexAsyncClient; + private final AzureAISearchVectorStoreOptions options; + + /** + * Creates a new instance of {@link AzureAISearchVectorStore}. + * + * @param searchIndexAsyncClient The Azure AI Search client. + * @param options The options for the vector store. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public AzureAISearchVectorStore(@Nonnull SearchIndexAsyncClient searchIndexAsyncClient, + @Nullable AzureAISearchVectorStoreOptions options) { + this.searchIndexAsyncClient = searchIndexAsyncClient; + this.options = options == null ? new AzureAISearchVectorStoreOptions() : options; + } + + /** + * Gets a new instance of {@link AzureAISearchVectorStoreRecordCollection} + * + * @param collectionName The name of the collection. + * @param options The options for the collection. + * @return The collection. + */ + @Override + public final VectorStoreRecordCollection getCollection( + @Nonnull String collectionName, + @Nonnull VectorStoreRecordCollectionOptions options) { + if (!options.getKeyClass().equals(String.class)) { + throw new SKException("Azure AI Search only supports string keys"); + } + if (options.getRecordClass() == null) { + throw new SKException("Record class is required"); + } + + if (this.options.getVectorStoreRecordCollectionFactory() != null) { + return (VectorStoreRecordCollection) this.options + .getVectorStoreRecordCollectionFactory() + .createVectorStoreRecordCollection( + searchIndexAsyncClient, + collectionName, + options.getRecordClass(), + options.getRecordDefinition()); + } + + return (VectorStoreRecordCollection) new AzureAISearchVectorStoreRecordCollection<>( + searchIndexAsyncClient, + collectionName, + (AzureAISearchVectorStoreRecordCollectionOptions) options); + } + + /** + * Gets the names of all collections in the Azure AI Search vector store. + * + * @return A list of collection names. + */ + @Override + public Mono> getCollectionNamesAsync() { + return searchIndexAsyncClient.listIndexes().map(SearchIndex::getName).collectList(); + } + + /** + * Creates a new {@link Builder} instance. + * + * @return The new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link AzureAISearchVectorStore}. + */ + public static class Builder { + + @Nullable + private SearchIndexAsyncClient searchIndexAsyncClient; + @Nullable + private AzureAISearchVectorStoreOptions options; + + /** + * Sets the Azure AI Search searchIndexClient. + * + * @param searchIndexAsyncClient The Azure AI Search searchIndexClient. + * @return The updated builder instance. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withSearchIndexAsyncClient( + @Nonnull SearchIndexAsyncClient searchIndexAsyncClient) { + this.searchIndexAsyncClient = searchIndexAsyncClient; + return this; + } + + /** + * Sets the options for the Azure AI Search vector store. + * + * @param options The options for the Azure AI Search vector store. + * @return The updated builder instance. + */ + public Builder withOptions( + @Nonnull AzureAISearchVectorStoreOptions options) { + this.options = options; + return this; + } + + /** + * Builds the Azure AI Search vector store. + * + * @return The Azure AI Search vector store. + */ + public AzureAISearchVectorStore build() { + if (searchIndexAsyncClient == null) { + throw new SKException("searchIndexAsyncClient is required"); + } + + return new AzureAISearchVectorStore(searchIndexAsyncClient, options); + } + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreCollectionCreateMapping.java b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreCollectionCreateMapping.java similarity index 62% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreCollectionCreateMapping.java rename to data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreCollectionCreateMapping.java index e077a5101..1b12b0ade 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreCollectionCreateMapping.java +++ b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreCollectionCreateMapping.java @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.azureaisearch; +package com.microsoft.semantickernel.data.azureaisearch; import com.azure.search.documents.indexes.models.ExhaustiveKnnAlgorithmConfiguration; import com.azure.search.documents.indexes.models.ExhaustiveKnnParameters; @@ -10,29 +10,33 @@ import com.azure.search.documents.indexes.models.VectorSearchAlgorithmConfiguration; import com.azure.search.documents.indexes.models.VectorSearchAlgorithmMetric; import com.azure.search.documents.indexes.models.VectorSearchProfile; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDataField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordKeyField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.exceptions.SKException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.time.OffsetDateTime; import java.util.List; -import java.util.Objects; +import javax.annotation.Nonnull; -public class AzureAISearchVectorStoreCollectionCreateMapping { +/** + * Maps vector store record fields to Azure AI Search fields. + */ +class AzureAISearchVectorStoreCollectionCreateMapping { private static String getVectorSearchProfileName(VectorStoreRecordVectorField vectorField) { - return vectorField.getName() + "Profile"; + return vectorField.getEffectiveStorageName() + "Profile"; } private static String getAlgorithmConfigName(VectorStoreRecordVectorField vectorField) { - return vectorField.getName() + "AlgorithmConfig"; + return vectorField.getEffectiveStorageName() + "AlgorithmConfig"; } private static VectorSearchAlgorithmMetric getAlgorithmMetric( @Nonnull VectorStoreRecordVectorField vectorField) { - if (vectorField.getDistanceFunction() == null) { + if (vectorField.getDistanceFunction() == DistanceFunction.UNDEFINED) { return VectorSearchAlgorithmMetric.COSINE; } @@ -41,17 +45,17 @@ private static VectorSearchAlgorithmMetric getAlgorithmMetric( return VectorSearchAlgorithmMetric.COSINE; case DOT_PRODUCT: return VectorSearchAlgorithmMetric.DOT_PRODUCT; - case EUCLIDEAN: + case EUCLIDEAN_DISTANCE: return VectorSearchAlgorithmMetric.EUCLIDEAN; default: - throw new IllegalArgumentException( + throw new SKException( "Unsupported distance function: " + vectorField.getDistanceFunction()); } } private static VectorSearchAlgorithmConfiguration getAlgorithmConfig( @Nonnull VectorStoreRecordVectorField vectorField) { - if (vectorField.getIndexKind() == null) { + if (vectorField.getIndexKind() == IndexKind.UNDEFINED) { return new HnswAlgorithmConfiguration(getAlgorithmConfigName(vectorField)) .setParameters(new HnswParameters().setMetric(getAlgorithmMetric(vectorField))); } @@ -65,49 +69,81 @@ private static VectorSearchAlgorithmConfiguration getAlgorithmConfig( .setParameters( new ExhaustiveKnnParameters().setMetric(getAlgorithmMetric(vectorField))); default: - throw new IllegalArgumentException( + throw new SKException( "Unsupported index kind: " + vectorField.getIndexKind()); } } + /** + * Maps a key field to a search field. + * + * @param keyField The key field. + * @return The search field. + */ public static SearchField mapKeyField(VectorStoreRecordKeyField keyField) { - return new SearchField(keyField.getName(), SearchFieldDataType.STRING) + return new SearchField(keyField.getEffectiveStorageName(), SearchFieldDataType.STRING) .setKey(true) .setFilterable(true); } + /** + * Maps a data field to a search field. + * + * @param dataField The data field. + * @return The search field. + */ public static SearchField mapDataField(VectorStoreRecordDataField dataField) { if (dataField.getFieldType() == null) { - throw new IllegalArgumentException( - "Field type is required: " + dataField.getName()); + throw new SKException( + "Field type is required: " + dataField.getEffectiveStorageName()); } - return new SearchField(dataField.getName(), + return new SearchField(dataField.getEffectiveStorageName(), getSearchFieldDataType(dataField.getFieldType())) - .setFilterable(dataField.isFilterable()); + .setFilterable(dataField.isFilterable()) + .setSearchable(dataField.isFullTextSearchable()); } + /** + * Maps a vector field to a search field. + * + * @param vectorField The vector field. + * @return The search field. + */ public static SearchField mapVectorField(VectorStoreRecordVectorField vectorField) { - return new SearchField(vectorField.getName(), + return new SearchField(vectorField.getEffectiveStorageName(), SearchFieldDataType.collection(SearchFieldDataType.SINGLE)) .setSearchable(true) .setVectorSearchDimensions(vectorField.getDimensions()) .setVectorSearchProfileName(getVectorSearchProfileName(vectorField)); } + /** + * Updates the vector search parameters for the specified vector field. + * + * @param algorithms The list of vector search algorithms. + * @param profiles The list of vector search profiles. + * @param vectorField The vector field. + */ public static void updateVectorSearchParameters( List algorithms, List profiles, VectorStoreRecordVectorField vectorField) { if (vectorField.getDimensions() <= 0) { - throw new IllegalArgumentException("Vector field dimensions must be greater than 0"); + throw new SKException("Vector field dimensions must be greater than 0"); } algorithms.add(getAlgorithmConfig(vectorField)); - profiles.add(new VectorSearchProfile(getVectorSearchProfileName(vectorField), - getAlgorithmConfigName(vectorField))); + profiles.add(new VectorSearchProfile( + getVectorSearchProfileName(vectorField), getAlgorithmConfigName(vectorField))); } + /** + * Gets the search field data type for the specified field type. + * + * @param fieldType The field type. + * @return The search field data type. + */ public static SearchFieldDataType getSearchFieldDataType(Class fieldType) { if (fieldType == String.class) { return SearchFieldDataType.STRING; @@ -124,7 +160,7 @@ public static SearchFieldDataType getSearchFieldDataType(Class fieldType) { } else if (fieldType == OffsetDateTime.class) { return SearchFieldDataType.DATE_TIME_OFFSET; } else { - throw new IllegalArgumentException("Unsupported field type: " + fieldType.getName()); + throw new SKException("Unsupported field type: " + fieldType.getName()); } } } diff --git a/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreCollectionSearchMapping.java b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreCollectionSearchMapping.java new file mode 100644 index 000000000..5d61f9817 --- /dev/null +++ b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreCollectionSearchMapping.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.azureaisearch; + +import com.microsoft.semantickernel.data.filter.AnyTagEqualToFilterClause; +import com.microsoft.semantickernel.data.filter.EqualToFilterClause; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.filter.FilterMapping; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.exceptions.SKException; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.stream.Collectors; + +class AzureAISearchVectorStoreCollectionSearchMapping + implements FilterMapping { + + private AzureAISearchVectorStoreCollectionSearchMapping() { + } + + private static class AzureAISearchVectorStoreCollectionSearchMappingHolder { + private static final AzureAISearchVectorStoreCollectionSearchMapping INSTANCE = new AzureAISearchVectorStoreCollectionSearchMapping(); + } + + static AzureAISearchVectorStoreCollectionSearchMapping getInstance() { + return AzureAISearchVectorStoreCollectionSearchMappingHolder.INSTANCE; + } + + public String getFilter(VectorSearchFilter vectorSearchFilter, + VectorStoreRecordDefinition recordDefinition) { + if (vectorSearchFilter == null + || vectorSearchFilter.getFilterClauses().isEmpty()) { + return ""; + } + + return vectorSearchFilter.getFilterClauses().stream().map(filterClause -> { + if (filterClause instanceof EqualToFilterClause) { + EqualToFilterClause equalToFilterClause = (EqualToFilterClause) filterClause; + // Create new instance with the storage name of the field + return getEqualToFilter(new EqualToFilterClause( + recordDefinition.getField(equalToFilterClause.getFieldName()) + .getEffectiveStorageName(), + equalToFilterClause.getValue())); + } else if (filterClause instanceof AnyTagEqualToFilterClause) { + AnyTagEqualToFilterClause anyTagEqualToFilterClause = (AnyTagEqualToFilterClause) filterClause; + // Create new instance with the storage name of the field + return getAnyTagEqualToFilter(new AnyTagEqualToFilterClause( + recordDefinition.getField(anyTagEqualToFilterClause.getFieldName()) + .getEffectiveStorageName(), + anyTagEqualToFilterClause.getValue())); + } else { + throw new SKException("Unsupported filter clause type '" + + filterClause.getClass().getSimpleName() + "'."); + } + }).collect(Collectors.joining(" and ")); + } + + @Override + public String getEqualToFilter(EqualToFilterClause filterClause) { + String fieldName = validateFieldName(filterClause.getFieldName()); + Object value = filterClause.getValue(); + + if (value instanceof String) { + return String.format("%s eq '%s'", fieldName, escapeSingleQuotes((String) value)); + } else if (value instanceof Boolean) { + return String.format("%s eq %s", fieldName, + value.toString().toLowerCase()); + } else if (value instanceof Integer) { + return String.format("%s eq %d", fieldName, (Integer) value); + } else if (value instanceof Long) { + return String.format("%s eq %d", fieldName, (Long) value); + } else if (value instanceof Float) { + return String.format("%s eq %f", fieldName, (Float) value); + } else if (value instanceof Double) { + return String.format("%s eq %f", fieldName, (Double) value); + } else if (value instanceof OffsetDateTime) { + return String.format("%s eq %s", fieldName, ((OffsetDateTime) value) + .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + } else if (value == null) { + return String.format("%s eq null", fieldName); + } else { + throw new SKException("Unsupported filter value type '" + + value.getClass().getSimpleName() + "'."); + } + } + + @Override + public String getAnyTagEqualToFilter(AnyTagEqualToFilterClause filterClause) { + return String.format("%s/any(t: t eq '%s')", validateFieldName(filterClause.getFieldName()), + escapeSingleQuotes(filterClause.getValue().toString())); + } + + private String validateFieldName(String fieldName) { + if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { + return fieldName; + } + throw new SKException("Invalid field name: " + fieldName); + } + + private String escapeSingleQuotes(String value) { + return value.replaceAll("'", "''"); + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreOptions.java b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreOptions.java similarity index 94% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreOptions.java rename to data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreOptions.java index d7bb0314d..23e907f2f 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreOptions.java +++ b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreOptions.java @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.azureaisearch; +package com.microsoft.semantickernel.data.azureaisearch; -import javax.annotation.Nonnull; import javax.annotation.Nullable; +/** + * Represents the options for the Azure AI Search vector store. + */ public class AzureAISearchVectorStoreOptions { + @Nullable private final AzureAISearchVectorStoreRecordCollectionFactory vectorStoreRecordCollectionFactory; @@ -49,6 +52,7 @@ public AzureAISearchVectorStoreRecordCollectionFactory getVectorStoreRecordColle * */ public static class Builder { + @Nullable private AzureAISearchVectorStoreRecordCollectionFactory vectorStoreRecordCollectionFactory; diff --git a/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollection.java b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollection.java new file mode 100644 index 000000000..b1c15b82f --- /dev/null +++ b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollection.java @@ -0,0 +1,471 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.azureaisearch; + +import com.azure.search.documents.SearchAsyncClient; +import com.azure.search.documents.SearchDocument; +import com.azure.search.documents.indexes.SearchIndexAsyncClient; +import com.azure.search.documents.indexes.models.SearchField; +import com.azure.search.documents.indexes.models.SearchIndex; +import com.azure.search.documents.indexes.models.VectorSearchAlgorithmConfiguration; +import com.azure.search.documents.indexes.models.VectorSearchProfile; +import com.azure.search.documents.models.IndexDocumentsResult; +import com.azure.search.documents.models.IndexingResult; +import com.azure.search.documents.models.ScoringParameter; +import com.azure.search.documents.models.SearchOptions; +import com.azure.search.documents.models.VectorQuery; +import com.azure.search.documents.models.VectorizableTextQuery; +import com.azure.search.documents.models.VectorizedQuery; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorsearch.VectorizableTextSearch; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorsearch.VectorizedSearch; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.DeleteRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Represents an Azure AI Search vector store record collection. + * + * @param The type of the record. + */ +public class AzureAISearchVectorStoreRecordCollection implements + VectorStoreRecordCollection, + VectorizedSearch, + VectorizableTextSearch { + + private static final HashSet> supportedKeyTypes = new HashSet<>( + Collections.singletonList( + String.class)); + + private static final HashSet> supportedDataTypes = new HashSet<>( + Arrays.asList( + String.class, + Integer.class, + int.class, + Long.class, + long.class, + Float.class, + float.class, + Double.class, + double.class, + Boolean.class, + boolean.class, + OffsetDateTime.class, + List.class)); + + private static final HashSet> supportedVectorTypes = new HashSet<>( + Arrays.asList( + List.class, + Collection.class)); + + private final SearchIndexAsyncClient searchIndexAsyncClient; + private final SearchAsyncClient searchAsyncClient; + private final String collectionName; + private final AzureAISearchVectorStoreRecordCollectionOptions options; + private final VectorStoreRecordDefinition recordDefinition; + + // List of non-vector fields. Used to fetch only non-vector fields when vectors are not requested + private final List nonVectorFields = new ArrayList<>(); + private final String firstVectorFieldName; + + /** + * Creates a new instance of {@link AzureAISearchVectorStoreRecordCollection}. + * + * @param searchIndexAsyncClient The Azure AI Search client. + * @param collectionName The name of the collection. + * @param options The options for the collection. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public AzureAISearchVectorStoreRecordCollection( + @Nonnull SearchIndexAsyncClient searchIndexAsyncClient, + @Nonnull String collectionName, + @Nonnull AzureAISearchVectorStoreRecordCollectionOptions options) { + this.searchIndexAsyncClient = searchIndexAsyncClient; + this.collectionName = collectionName; + this.searchAsyncClient = searchIndexAsyncClient.getSearchAsyncClient(collectionName); + this.options = options; + + // If record definition is not provided, create one from the record class + this.recordDefinition = options.getRecordDefinition() == null + ? VectorStoreRecordDefinition.fromRecordClass(options.getRecordClass()) + : options.getRecordDefinition(); + + // Validate supported types + VectorStoreRecordDefinition.validateSupportedTypes( + Collections.singletonList(recordDefinition.getKeyField()), + supportedKeyTypes); + VectorStoreRecordDefinition.validateSupportedTypes( + new ArrayList<>(recordDefinition.getDataFields()), + supportedDataTypes); + VectorStoreRecordDefinition.validateSupportedTypes( + new ArrayList<>(recordDefinition.getVectorFields()), + supportedVectorTypes); + + // Add non-vector fields to the list + nonVectorFields.add(this.recordDefinition.getKeyField().getEffectiveStorageName()); + nonVectorFields.addAll(this.recordDefinition.getDataFields().stream() + .map(VectorStoreRecordDataField::getEffectiveStorageName) + .collect(Collectors.toList())); + + firstVectorFieldName = recordDefinition.getVectorFields().isEmpty() ? null + : recordDefinition.getVectorFields().get(0).getName(); + } + + @Override + public String getCollectionName() { + return collectionName; + } + + private Mono> getIndexesAsync() { + return searchIndexAsyncClient.listIndexes().map(SearchIndex::getName) + .collect(Collectors.toList()); + } + + @Override + public Mono collectionExistsAsync() { + return getIndexesAsync() + .map(list -> list.stream().anyMatch(name -> name.equalsIgnoreCase(collectionName))); + } + + @Override + public Mono> createCollectionAsync() { + List searchFields = new ArrayList<>(); + List algorithms = new ArrayList<>(); + List profiles = new ArrayList<>(); + + for (VectorStoreRecordField field : this.recordDefinition.getAllFields()) { + if (field instanceof VectorStoreRecordKeyField) { + searchFields.add(AzureAISearchVectorStoreCollectionCreateMapping + .mapKeyField((VectorStoreRecordKeyField) field)); + } else if (field instanceof VectorStoreRecordDataField) { + searchFields.add(AzureAISearchVectorStoreCollectionCreateMapping + .mapDataField((VectorStoreRecordDataField) field)); + } else { + searchFields.add(AzureAISearchVectorStoreCollectionCreateMapping + .mapVectorField((VectorStoreRecordVectorField) field)); + AzureAISearchVectorStoreCollectionCreateMapping + .updateVectorSearchParameters(algorithms, profiles, + (VectorStoreRecordVectorField) field); + } + } + + SearchIndex newIndex = new SearchIndex(collectionName) + .setFields(searchFields) + .setVectorSearch(new com.azure.search.documents.indexes.models.VectorSearch() + .setAlgorithms(algorithms) + .setProfiles(profiles)); + + return searchIndexAsyncClient.createIndex(newIndex).then(Mono.just(this)); + } + + @Override + public Mono> createCollectionIfNotExistsAsync() { + return collectionExistsAsync().flatMap( + exists -> { + if (!exists) { + return createCollectionAsync(); + } + return Mono.empty(); + }) + .then(Mono.just(this)); + } + + @Override + public Mono deleteCollectionAsync() { + return searchIndexAsyncClient.deleteIndex(this.collectionName).then(); + } + + @Override + public Mono getAsync( + @Nonnull String key, GetRecordOptions options) { + // If vectors are not requested, only fetch non-vector fields + List selectedFields = null; + if (options == null || !options.isIncludeVectors()) { + selectedFields = Collections.unmodifiableList(nonVectorFields); + } + + VectorStoreRecordMapper mapper = this.options + .getVectorStoreRecordMapper(); + + // Use custom mapper if available + if (mapper != null && mapper.getStorageModelToRecordMapper() != null) { + return searchAsyncClient.getDocument(key, SearchDocument.class) + .map(record -> mapper.mapStorageModelToRecord(record, options)); + } + + return searchAsyncClient + .getDocumentWithResponse(key, this.options.getRecordClass(), selectedFields) + .flatMap(response -> { + int statusCode = response.getStatusCode(); + if (statusCode >= 200 && statusCode < 300) { + return Mono.just(response.getValue()); + } + if (response.getStatusCode() == 404) { + return Mono.error(new SKException("Record not found: " + key)); + } + return Mono.error(new SKException("Failed to get record: " + key + ". Status code: " + + statusCode)); + }); + + } + + @Override + public Mono> getBatchAsync( + @Nonnull List keys, + GetRecordOptions options) { + return Flux.fromIterable(keys) + .flatMap(key -> getAsync(key, options).flux()) + .collect(Collectors.toList()); + } + + @Override + public Mono upsertAsync(@Nonnull Record record, UpsertRecordOptions options) { + return upsertBatchAsync(Collections.singletonList(record), options) + .map(Collection::iterator) + .map(Iterator::next); + } + + @Override + public Mono> upsertBatchAsync( + @Nonnull List records, UpsertRecordOptions options) { + if (records.isEmpty()) { + return Mono.just(Collections.emptyList()); + } + + VectorStoreRecordMapper mapper = this.options + .getVectorStoreRecordMapper(); + Iterable documents; + + // Use custom mapper if available + if (mapper != null && mapper.getRecordToStorageModelMapper() != null) { + documents = records.stream() + .map(this.options.getVectorStoreRecordMapper()::mapRecordToStorageModel) + .collect(Collectors.toList()); + } else { + documents = records; + } + + return searchAsyncClient.uploadDocuments(documents) + .map(IndexDocumentsResult::getResults) + .map( + results -> results.stream() + .map(IndexingResult::getKey) + .collect(Collectors.toList())); + } + + @Override + public Mono deleteAsync(String key, DeleteRecordOptions options) { + return deleteBatchAsync(Collections.singletonList(key), options); + } + + @Override + public Mono deleteBatchAsync(List keys, DeleteRecordOptions options) { + return searchAsyncClient.deleteDocuments(keys.stream().map(key -> { + SearchDocument document = new SearchDocument(); + document.put(this.recordDefinition.getKeyField().getEffectiveStorageName(), key); + return document; + }).collect(Collectors.toList())).then(); + } + + private SearchOptions configureVectorSearchOptions( + List vectorQueries, VectorSearchOptions options) { + String filter = AzureAISearchVectorStoreCollectionSearchMapping.getInstance() + .getFilter(options.getVectorSearchFilter(), recordDefinition); + + SearchOptions searchOptions = new SearchOptions() + .setFilter(filter) + .setTop(options.getTop()) + .setSkip(options.getSkip()) + .setVectorSearchOptions(new com.azure.search.documents.models.VectorSearchOptions() + .setQueries(vectorQueries)); + + if (!options.isIncludeVectors()) { + searchOptions.setSelect(nonVectorFields.toArray(new String[0])); + } + + return searchOptions; + } + + private Mono> searchAndMapAsync(String query, + SearchOptions searchOptions, + boolean includeVectors) { + VectorStoreRecordMapper mapper = this.options + .getVectorStoreRecordMapper(); + + return this.searchAsyncClient.search(query, searchOptions) + .flatMap(response -> { + Record record; + + // Use custom mapper if available + if (mapper != null && mapper.getStorageModelToRecordMapper() != null) { + record = mapper + .mapStorageModelToRecord(response.getDocument(SearchDocument.class), + new GetRecordOptions(includeVectors)); + } else { + record = response.getDocument(this.options.getRecordClass()); + } + + return Mono.just(new VectorSearchResult<>(record, response.getScore())); + }).collectList().flatMap(results -> Mono.just( + new VectorSearchResults<>(results))); + } + + /** + * Vectorizable text search. This method searches for records that are similar to the given text after vectorization. + *

+ * Vectorizer configuration must be set up in the Azure AI Search index. + * + * @param searchText The text to search with. + * @param options The options to use for the search. + * @return A list of search results. + */ + @Override + public Mono> searchAsync(String searchText, + VectorSearchOptions options) { + if (firstVectorFieldName == null) { + throw new SKException("No vector fields defined. Cannot perform vector search"); + } + + if (options == null) { + options = VectorSearchOptions.createDefault(firstVectorFieldName); + } + + List vectorQueries = new ArrayList<>(); + vectorQueries.add(new VectorizableTextQuery(searchText) + .setFields(recordDefinition.getField(options.getVectorFieldName() != null + ? options.getVectorFieldName() + : firstVectorFieldName).getEffectiveStorageName()) + .setKNearestNeighborsCount(options.getTop())); + + return searchAndMapAsync(null, + configureVectorSearchOptions(vectorQueries, options), + options.isIncludeVectors()); + } + + /** + * Vectorized search. This method searches for records that are similar to the given vector. + * + * @param vector The vector to search with. + * @param options The options to use for the search. + * @return A list of search results. + */ + @Override + public Mono> searchAsync(List vector, + VectorSearchOptions options) { + return hybridSearchAsync(null, vector, options, null); + } + + /** + * Hybrid search. This method searches for records that are similar to the given text and vector. + * + * @param searchText The text to search with. + * If null, only vector search is performed. + * @param vector The vector to search with. + * If null, only full text search is performed. + * @param options The vector search options used for the search. + * @param additionalSearchOptions AzureAI search additional options. + * If Filter, Top, Skip, Select or VectorSearchOptions are not null, they will be used instead of the default options. + *

+ * If null, default search options are used. + */ + public Mono> hybridSearchAsync(String searchText, + List vector, VectorSearchOptions options, SearchOptions additionalSearchOptions) { + SearchOptions searchOptions = new SearchOptions(); + + if (vector != null) { + if (firstVectorFieldName == null) { + throw new SKException("No vector fields defined. Cannot perform vector search"); + } + + if (options == null) { + options = VectorSearchOptions.createDefault(firstVectorFieldName); + } + + List vectorQueries = new ArrayList<>(); + vectorQueries.add(new VectorizedQuery(vector) + .setFields(recordDefinition.getField(options.getVectorFieldName() != null + ? options.getVectorFieldName() + : firstVectorFieldName).getEffectiveStorageName()) + .setKNearestNeighborsCount(options.getTop())); + + // Configure default vector search options + searchOptions = configureVectorSearchOptions(vectorQueries, options); + } + + // Configure additional search options + if (additionalSearchOptions != null) { + searchOptions + .setQueryType(additionalSearchOptions.getQueryType()) + .setSemanticSearchOptions(additionalSearchOptions.getSemanticSearchOptions()) + .setFacets(additionalSearchOptions.getFacets() != null + ? additionalSearchOptions.getFacets().toArray(new String[0]) + : null) + .setHighlightFields(additionalSearchOptions.getHighlightFields() != null + ? additionalSearchOptions.getHighlightFields().toArray(new String[0]) + : null) + .setHighlightPreTag(additionalSearchOptions.getHighlightPreTag()) + .setHighlightPostTag(additionalSearchOptions.getHighlightPostTag()) + .setMinimumCoverage(additionalSearchOptions.getMinimumCoverage()) + .setOrderBy(additionalSearchOptions.getOrderBy() != null + ? additionalSearchOptions.getOrderBy().toArray(new String[0]) + : null) + .setScoringParameters(additionalSearchOptions.getScoringParameters() != null + ? additionalSearchOptions.getScoringParameters().stream() + .map(s -> new ScoringParameter(s.getName(), s.getValues())) + .toArray(ScoringParameter[]::new) + : null) + .setScoringProfile(additionalSearchOptions.getScoringProfile()) + .setSearchFields(additionalSearchOptions.getSearchFields() != null + ? additionalSearchOptions.getSearchFields().toArray(new String[0]) + : null) + .setIncludeTotalCount(additionalSearchOptions.isTotalCountIncluded()) + .setSearchMode(additionalSearchOptions.getSearchMode()) + .setScoringStatistics(additionalSearchOptions.getScoringStatistics()) + .setSessionId(additionalSearchOptions.getSessionId()); + + // Override default vector options if provided + if (additionalSearchOptions.getFilter() != null) { + searchOptions.setFilter(additionalSearchOptions.getFilter()); + } + if (additionalSearchOptions.getTop() != null) { + searchOptions.setTop(additionalSearchOptions.getTop()); + } + if (additionalSearchOptions.getSkip() != null) { + searchOptions.setSkip(additionalSearchOptions.getSkip()); + } + if (additionalSearchOptions.getVectorSearchOptions() != null) { + searchOptions + .setVectorSearchOptions(additionalSearchOptions.getVectorSearchOptions()); + } + if (additionalSearchOptions.getSelect() != null) { + searchOptions.setSelect(additionalSearchOptions.getSelect().toArray(new String[0])); + } + } + + return searchAndMapAsync(searchText, searchOptions, + options != null && options.isIncludeVectors()); + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionFactory.java b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionFactory.java similarity index 58% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionFactory.java rename to data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionFactory.java index c50412847..ada0f70be 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionFactory.java +++ b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionFactory.java @@ -1,24 +1,27 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.azureaisearch; +package com.microsoft.semantickernel.data.azureaisearch; import com.azure.search.documents.indexes.SearchIndexAsyncClient; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; /** * Factory for creating Azure AI Search vector store record collections. - * */ public interface AzureAISearchVectorStoreRecordCollectionFactory { /** * Creates a new Azure AI Search vector store record collection. * - * @param client The Azure AI Search client. + * @param client The Azure AI Search client. * @param collectionName The name of the collection. - * @param options The options for the collection. + * @param recordClass The class type of the record. + * @param recordDefinition The record definition. + * @param The record type. * @return The new Azure AI Search vector store record collection. */ AzureAISearchVectorStoreRecordCollection createVectorStoreRecordCollection( SearchIndexAsyncClient client, String collectionName, - AzureAISearchVectorStoreRecordCollectionOptions options); + Class recordClass, + VectorStoreRecordDefinition recordDefinition); } diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionOptions.java b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionOptions.java similarity index 81% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionOptions.java rename to data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionOptions.java index 45fb410cb..2cd143607 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionOptions.java +++ b/data/semantickernel-data-azureaisearch/src/main/java/com/microsoft/semantickernel/data/azureaisearch/AzureAISearchVectorStoreRecordCollectionOptions.java @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.azureaisearch; +package com.microsoft.semantickernel.data.azureaisearch; import com.azure.search.documents.SearchDocument; -import com.microsoft.semantickernel.data.VectorStoreRecordMapper; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.exceptions.SKException; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -13,11 +15,11 @@ * * @param the record type */ -public class AzureAISearchVectorStoreRecordCollectionOptions { +public class AzureAISearchVectorStoreRecordCollectionOptions + implements VectorStoreRecordCollectionOptions { private final Class recordClass; @Nullable private final VectorStoreRecordMapper vectorStoreRecordMapper; - @Nullable private final VectorStoreRecordDefinition recordDefinition; @@ -31,6 +33,16 @@ public static Builder builder() { return new Builder<>(); } + /** + * Gets the key class. + * + * @return the key class + */ + @Override + public Class getKeyClass() { + return String.class; + } + /** * Gets the record class. * @@ -75,6 +87,7 @@ private AzureAISearchVectorStoreRecordCollectionOptions( * @param the record type */ public static class Builder { + @Nullable private VectorStoreRecordMapper vectorStoreRecordMapper; @Nullable @@ -82,6 +95,11 @@ public static class Builder { @Nullable private VectorStoreRecordDefinition recordDefinition; + /** + * Sets the record class. + * @param recordClass the record Class + * @return the builder + */ public Builder withRecordClass(Class recordClass) { this.recordClass = recordClass; return this; @@ -117,7 +135,7 @@ public Builder withRecordDefinition(VectorStoreRecordDefinition recordDe */ public AzureAISearchVectorStoreRecordCollectionOptions build() { if (recordClass == null) { - throw new IllegalArgumentException("recordClass must be provided"); + throw new SKException("recordClass must be provided"); } return new AzureAISearchVectorStoreRecordCollectionOptions<>( diff --git a/connectors/semantickernel-connectors-memory-redis/pom.xml b/data/semantickernel-data-hsqldb/pom.xml similarity index 63% rename from connectors/semantickernel-connectors-memory-redis/pom.xml rename to data/semantickernel-data-hsqldb/pom.xml index 51a340d01..bb06dfe4a 100644 --- a/connectors/semantickernel-connectors-memory-redis/pom.xml +++ b/data/semantickernel-data-hsqldb/pom.xml @@ -4,50 +4,45 @@ com.microsoft.semantic-kernel semantickernel-parent - 0.2.14-alpha-SNAPSHOT + 1.5.1-SNAPSHOT ../../pom.xml - semantickernel-connectors-memory-redis - - Semantic Kernel Redis Memory Connector - Defines connector for Redis to be used with the Semantic Kernel + com.microsoft.semantic-kernel + semantickernel-data-hsqldb + Semantic Kernel HLSQLDB connector + Provides a HLSQLDB connector for the Semantic Kernel - com.fasterxml.jackson.core - jackson-core - compile + com.microsoft.semantic-kernel + semantickernel-api - com.fasterxml.jackson.core - jackson-databind - compile + com.microsoft.semantic-kernel + semantickernel-data-jdbc com.microsoft.semantic-kernel - semantickernel-api + semantickernel-api-exceptions - org.mockito - mockito-core - test + com.microsoft.semantic-kernel + semantickernel-api-data - org.junit.jupiter - junit-jupiter - test + com.microsoft.semantic-kernel + semantickernel-api-builders + - redis.clients - jedis - 5.1.0 - provided + com.fasterxml.jackson.core + jackson-databind + compile - com.microsoft.semantic-kernel - semantickernel-connectors-memory-jdbc - ${project.version} + com.fasterxml.jackson.core + jackson-core compile diff --git a/data/semantickernel-data-hsqldb/src/main/java/com/microsoft/semantickernel/data/jdbc/hsqldb/HSQLDBVectorStoreQueryProvider.java b/data/semantickernel-data-hsqldb/src/main/java/com/microsoft/semantickernel/data/jdbc/hsqldb/HSQLDBVectorStoreQueryProvider.java new file mode 100644 index 000000000..e105a20db --- /dev/null +++ b/data/semantickernel-data-hsqldb/src/main/java/com/microsoft/semantickernel/data/jdbc/hsqldb/HSQLDBVectorStoreQueryProvider.java @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc.hsqldb; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.sql.DataSource; + +/** + * The HSQLDB vector store query provider. + * Provides the necessary methods to interact with a HSQLDB vector store and vector store collections. + */ +public class HSQLDBVectorStoreQueryProvider extends JDBCVectorStoreQueryProvider { + + private final ObjectMapper objectMapper; + + private HSQLDBVectorStoreQueryProvider( + DataSource dataSource, + String collectionsTable, + String prefixForCollectionTables, + int defaultVarCharLength, + ObjectMapper objectMapper) { + super( + dataSource, + collectionsTable, + prefixForCollectionTables, + buildSupportedKeyTypes(defaultVarCharLength), + buildSupportedDataTypes(defaultVarCharLength), + buildSupportedVectorTypes(defaultVarCharLength)); + this.objectMapper = objectMapper; + } + + private static Map, String> buildSupportedVectorTypes(int defaultVarCharLength) { + HashMap, String> supportedVectorTypes = new HashMap<>(); + supportedVectorTypes.put(String.class, "VARCHAR(" + defaultVarCharLength + ")"); + supportedVectorTypes.put(List.class, "VARCHAR(" + defaultVarCharLength + ")"); + supportedVectorTypes.put(Collection.class, "VARCHAR(" + defaultVarCharLength + ")"); + return supportedVectorTypes; + } + + private static Map, String> buildSupportedDataTypes(int defaultVarCharLength) { + HashMap, String> supportedDataTypes = new HashMap<>(); + supportedDataTypes.put(String.class, "VARCHAR(" + defaultVarCharLength + ")"); + supportedDataTypes.put(Integer.class, "INTEGER"); + supportedDataTypes.put(int.class, "INTEGER"); + supportedDataTypes.put(Long.class, "BIGINT"); + supportedDataTypes.put(long.class, "BIGINT"); + supportedDataTypes.put(Float.class, "REAL"); + supportedDataTypes.put(float.class, "REAL"); + supportedDataTypes.put(Double.class, "DOUBLE"); + supportedDataTypes.put(double.class, "DOUBLE"); + supportedDataTypes.put(Boolean.class, "BOOLEAN"); + supportedDataTypes.put(boolean.class, "BOOLEAN"); + supportedDataTypes.put(OffsetDateTime.class, "TIMESTAMPTZ"); + supportedDataTypes.put(List.class, "TEXT"); + return supportedDataTypes; + } + + private static HashMap, String> buildSupportedKeyTypes(int defaultVarCharLength) { + HashMap, String> supportedKeyTypes = new HashMap<>(); + supportedKeyTypes.put(String.class, "VARCHAR(" + defaultVarCharLength + ")"); + return supportedKeyTypes; + } + + private void setUpsertStatementValues(PreparedStatement statement, Object record, + List fields) { + JsonNode jsonNode = objectMapper.valueToTree(record); + + for (int i = 0; i < fields.size(); ++i) { + VectorStoreRecordField field = fields.get(i); + try { + JsonNode valueNode = jsonNode.get(field.getEffectiveStorageName()); + + if (field instanceof VectorStoreRecordVectorField) { + // Convert the vector field to a string + if (!field.getFieldType().equals(String.class)) { + statement.setObject(i + 1, objectMapper.writeValueAsString(valueNode)); + continue; + } + } else if (field instanceof VectorStoreRecordDataField) { + // Convert List field to a string + if (field.getFieldType().equals(List.class)) { + statement.setObject(i + 1, objectMapper.writeValueAsString(valueNode)); + continue; + } + } + + statement.setObject(i + 1, + objectMapper.convertValue(valueNode, field.getFieldType())); + } catch (SQLException | JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Upserts records into the collection. + * + * @param collectionName the collection name + * @param records the records to upsert + * @param recordDefinition the record definition + * @param options the upsert options + * @throws SKException if the upsert fails + */ + @Override + @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") + // SQL query is generated dynamically with valid identifiers + public void upsertRecords(String collectionName, List records, + VectorStoreRecordDefinition recordDefinition, UpsertRecordOptions options) { + validateSQLidentifier(getCollectionTableName(collectionName)); + + List fields = recordDefinition.getAllFields(); + + String keyName = recordDefinition.getKeyField().getStorageName(); + + String updater = fields + .stream() + .map(VectorStoreRecordField::getStorageName) + .map(it -> "t." + it + "=vals." + it) + .collect(Collectors.joining(",")); + + String setter = fields + .stream() + .map(VectorStoreRecordField::getStorageName) + .map(it -> "vals." + it) + .collect(Collectors.joining(",")); + + String query = formatQuery( + "MERGE INTO %s AS t USING (VALUES (%s)) AS vals(%s) " + + "ON t.%s=vals.%s WHEN MATCHED THEN UPDATE SET %s " + + "WHEN NOT MATCHED THEN INSERT VALUES %s", + getCollectionTableName(collectionName), + getWildcardString(fields.size()), + getQueryColumnsFromFields(fields), + keyName, + keyName, + updater, + setter); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + for (Object record : records) { + setUpsertStatementValues(statement, record, recordDefinition.getAllFields()); + statement.addBatch(); + } + + statement.executeBatch(); + } catch (SQLException e) { + throw new SKException("Failed to upsert records", e); + } + } + + /** + * Creates a new builder. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * The builder for the HSQLDB vector store query provider. + */ + public static class Builder + extends JDBCVectorStoreQueryProvider.Builder { + + private DataSource dataSource; + private String collectionsTable = DEFAULT_COLLECTIONS_TABLE; + private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; + private int defaultVarCharLength = 255; + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Sets the data source. + * + * @param dataSource the data source + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withDataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + /** + * Sets the collections table name. + * + * @param collectionsTable the collections table name + * @return the builder + */ + public Builder withCollectionsTable(String collectionsTable) { + this.collectionsTable = validateSQLidentifier(collectionsTable); + return this; + } + + /** + * Sets the prefix for collection tables. + * + * @param prefixForCollectionTables the prefix for collection tables + * @return the builder + */ + public Builder withPrefixForCollectionTables(String prefixForCollectionTables) { + this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); + return this; + } + + /** + * Sets the default VARCHAR length. + * + * @param defaultVarCharLength the default VARCHAR length + * @return the builder + */ + public Builder setDefaultVarCharLength(int defaultVarCharLength) { + this.defaultVarCharLength = defaultVarCharLength; + return this; + } + + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Builds the HSQLDB vector store query provider. + * + * @return the HSQLDB vector store query provider + */ + public HSQLDBVectorStoreQueryProvider build() { + if (dataSource == null) { + throw new SKException("DataSource is required"); + } + + return new HSQLDBVectorStoreQueryProvider( + dataSource, + collectionsTable, + prefixForCollectionTables, + defaultVarCharLength, + objectMapper); + } + + } +} diff --git a/data/semantickernel-data-jdbc/pom.xml b/data/semantickernel-data-jdbc/pom.xml new file mode 100644 index 000000000..cf8069ec0 --- /dev/null +++ b/data/semantickernel-data-jdbc/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../../pom.xml + + + semantickernel-data-jdbc + Semantic Kernel JDBC connector + Provides a JDBC connector for the Semantic Kernel + + + + com.microsoft.semantic-kernel + semantickernel-api-data + provided + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + provided + + + com.microsoft.semantic-kernel + semantickernel-api-builders + provided + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.core + jackson-databind + compile + + + com.fasterxml.jackson.core + jackson-core + compile + + + com.github.jknack + handlebars + + + com.google.code.findbugs + jsr305 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + compile + + + com.github.spotbugs + spotbugs-annotations + + + org.apache.commons + commons-text + + + org.postgresql + postgresql + 42.7.12 + + + org.xerial + sqlite-jdbc + 3.53.0.0 + + + com.oracle.database.jdbc + ojdbc11 + 23.26.1.0.0 + + + \ No newline at end of file diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStore.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStore.java similarity index 60% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStore.java rename to data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStore.java index 5e4971769..ddc2f7501 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStore.java +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStore.java @@ -1,27 +1,30 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; +package com.microsoft.semantickernel.data.jdbc; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.exceptions.SKException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; - +import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.sql.DataSource; -import java.util.List; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; /** * A JDBC vector store. */ -public class JDBCVectorStore implements SQLVectorStore> { +public class JDBCVectorStore implements SQLVectorStore { + private final DataSource dataSource; private final JDBCVectorStoreOptions options; - private final JDBCVectorStoreQueryProvider queryProvider; + private final SQLVectorStoreQueryProvider queryProvider; /** - * Creates a new instance of the {@link JDBCVectorStore}. - * If using this constructor, call {@link #prepareAsync()} before using the vector store. + * Creates a new instance of the {@link JDBCVectorStore}. If using this constructor, call + * {@link #prepareAsync()} before using the vector store. * * @param dataSource the connection * @param options the options @@ -35,7 +38,7 @@ public JDBCVectorStore(@Nonnull DataSource dataSource, if (this.options != null && this.options.getQueryProvider() != null) { this.queryProvider = this.options.getQueryProvider(); } else { - this.queryProvider = JDBCVectorStoreDefaultQueryProvider.builder() + this.queryProvider = JDBCVectorStoreQueryProvider.builder() .withDataSource(dataSource) .build(); } @@ -53,31 +56,60 @@ public static Builder builder() { /** * Gets a collection from the vector store. * - * @param collectionName The name of the collection. - * @param recordClass The class type of the record. - * @param recordDefinition The record definition. + * @param collectionName The name of the collection. + * @param options The options for the collection. * @return The collection. */ @Override - public JDBCVectorStoreRecordCollection getCollection( + public VectorStoreRecordCollection getCollection( @Nonnull String collectionName, - @Nonnull Class recordClass, - @Nullable VectorStoreRecordDefinition recordDefinition) { + @Nonnull VectorStoreRecordCollectionOptions options) { + if (!options.getKeyClass().equals(String.class)) { + throw new SKException("JDBC only supports string keys"); + } + if (options.getRecordClass() == null) { + throw new SKException("Record class is required"); + } if (this.options != null && this.options.getVectorStoreRecordCollectionFactory() != null) { - return this.options.getVectorStoreRecordCollectionFactory() + return (VectorStoreRecordCollection) this.options + .getVectorStoreRecordCollectionFactory() .createVectorStoreRecordCollection( dataSource, collectionName, - JDBCVectorStoreRecordCollectionOptions.builder() - .withRecordClass(recordClass) - .withRecordDefinition(recordDefinition) - .withQueryProvider(this.queryProvider) - .build()); + options.getRecordClass(), + options.getRecordDefinition()); } - return new JDBCVectorStoreRecordCollection<>( + JDBCVectorStoreRecordCollectionOptions jdbcOptions = (JDBCVectorStoreRecordCollectionOptions) options; + return (VectorStoreRecordCollection) new JDBCVectorStoreRecordCollection<>( dataSource, + collectionName, + JDBCVectorStoreRecordCollectionOptions.builder() + .withCollectionsTableName(jdbcOptions.getCollectionsTableName()) + .withPrefixForCollectionTables(jdbcOptions.getPrefixForCollectionTables()) + .withQueryProvider(jdbcOptions.getQueryProvider() == null ? queryProvider + : jdbcOptions.getQueryProvider()) + .withRecordClass(jdbcOptions.getRecordClass()) + .withRecordDefinition(jdbcOptions.getRecordDefinition()) + .withVectorStoreRecordMapper(jdbcOptions.getVectorStoreRecordMapper()) + .build()); + } + + /** + * Gets a collection from the vector store. + * + * @param collectionName The name of the collection. + * @param recordClass The class type of the record. + * @param recordDefinition The record definition. + * @param The record type. + * @return The collection. + */ + public VectorStoreRecordCollection getCollection( + @Nonnull String collectionName, + @Nonnull Class recordClass, + @Nullable VectorStoreRecordDefinition recordDefinition) { + return getCollection( collectionName, JDBCVectorStoreRecordCollectionOptions.builder() .withRecordClass(recordClass) @@ -110,6 +142,7 @@ public Mono prepareAsync() { * Builder for creating a {@link JDBCVectorStore}. */ public static class Builder { + private DataSource dataSource; private JDBCVectorStoreOptions options; @@ -152,7 +185,7 @@ public JDBCVectorStore build() { */ public Mono buildAsync() { if (dataSource == null) { - throw new IllegalArgumentException("dataSource is required"); + throw new SKException("dataSource is required"); } JDBCVectorStore vectorStore = new JDBCVectorStore(dataSource, options); diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreOptions.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreOptions.java similarity index 86% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreOptions.java rename to data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreOptions.java index adb6e13c3..729ebbbdc 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreOptions.java +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreOptions.java @@ -1,24 +1,29 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; +package com.microsoft.semantickernel.data.jdbc; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; +/** + * Options for the JDBC vector store. + */ public class JDBCVectorStoreOptions { @Nullable private final JDBCVectorStoreRecordCollectionFactory vectorStoreRecordCollectionFactory; @Nullable - private final JDBCVectorStoreQueryProvider queryProvider; + private final SQLVectorStoreQueryProvider queryProvider; /** * Creates a new instance of the JDBC vector store options. * * @param vectorStoreRecordCollectionFactory The vector store record collection factory. + * @param queryProvider The query provider. + * */ @SuppressFBWarnings("EI_EXPOSE_REP2") // DataSource in queryProvider is not exposed public JDBCVectorStoreOptions( - @Nullable JDBCVectorStoreQueryProvider queryProvider, + @Nullable SQLVectorStoreQueryProvider queryProvider, @Nullable JDBCVectorStoreRecordCollectionFactory vectorStoreRecordCollectionFactory) { this.queryProvider = queryProvider; this.vectorStoreRecordCollectionFactory = vectorStoreRecordCollectionFactory; @@ -38,7 +43,7 @@ public JDBCVectorStoreOptions() { */ @Nullable @SuppressFBWarnings("EI_EXPOSE_REP") // DataSource in queryProvider is not exposed - public JDBCVectorStoreQueryProvider getQueryProvider() { + public SQLVectorStoreQueryProvider getQueryProvider() { return queryProvider; } @@ -63,11 +68,10 @@ public JDBCVectorStoreRecordCollectionFactory getVectorStoreRecordCollectionFact /** * Builder for JDBC vector store options. - * */ public static class Builder { @Nullable - private JDBCVectorStoreQueryProvider queryProvider; + private SQLVectorStoreQueryProvider queryProvider; @Nullable private JDBCVectorStoreRecordCollectionFactory vectorStoreRecordCollectionFactory; @@ -78,7 +82,7 @@ public static class Builder { * @return The updated builder instance. */ @SuppressFBWarnings("EI_EXPOSE_REP2") // DataSource in queryProvider is not exposed - public Builder withQueryProvider(JDBCVectorStoreQueryProvider queryProvider) { + public Builder withQueryProvider(SQLVectorStoreQueryProvider queryProvider) { this.queryProvider = queryProvider; return this; } diff --git a/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreQueryProvider.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreQueryProvider.java new file mode 100644 index 000000000..3441ccfed --- /dev/null +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreQueryProvider.java @@ -0,0 +1,767 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc; + +import com.microsoft.semantickernel.data.filter.AnyTagEqualToFilterClause; +import com.microsoft.semantickernel.data.filter.EqualToFilterClause; +import com.microsoft.semantickernel.data.vectorsearch.VectorOperations; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.DeleteRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A JDBC vector store query provider. + */ +public class JDBCVectorStoreQueryProvider + implements SQLVectorStoreQueryProvider, + SQLVectorStoreFilterQueryProvider { + + private static final Logger LOGGER = LoggerFactory + .getLogger(JDBCVectorStoreQueryProvider.class); + + protected final Map, String> supportedKeyTypes; + protected final Map, String> supportedDataTypes; + protected final Map, String> supportedVectorTypes; + + protected final DataSource dataSource; + private final String collectionsTable; + private final String prefixForCollectionTables; + + private final Object dbCreationLock = new Object(); + + protected JDBCVectorStoreQueryProvider( + @Nonnull DataSource dataSource, + @Nonnull String collectionsTable, + @Nonnull String prefixForCollectionTables) { + this.dataSource = dataSource; + this.collectionsTable = collectionsTable; + this.prefixForCollectionTables = prefixForCollectionTables; + + supportedKeyTypes = new HashMap<>(); + supportedKeyTypes.put(String.class, "VARCHAR(255)"); + + supportedDataTypes = new HashMap<>(); + supportedDataTypes.put(String.class, "TEXT"); + supportedDataTypes.put(Integer.class, "INTEGER"); + supportedDataTypes.put(int.class, "INTEGER"); + supportedDataTypes.put(Long.class, "BIGINT"); + supportedDataTypes.put(long.class, "BIGINT"); + supportedDataTypes.put(Float.class, "REAL"); + supportedDataTypes.put(float.class, "REAL"); + supportedDataTypes.put(Double.class, "DOUBLE"); + supportedDataTypes.put(double.class, "DOUBLE"); + supportedDataTypes.put(Boolean.class, "BOOLEAN"); + supportedDataTypes.put(boolean.class, "BOOLEAN"); + supportedDataTypes.put(OffsetDateTime.class, "TIMESTAMPTZ"); + supportedDataTypes.put(List.class, "TEXT"); + + supportedVectorTypes = new HashMap<>(); + supportedVectorTypes.put(String.class, "TEXT"); + supportedVectorTypes.put(List.class, "TEXT"); + supportedVectorTypes.put(Collection.class, "TEXT"); + } + + /** + * Creates a new instance of the JDBCVectorStoreQueryProvider class. + * + * @param dataSource the data source + * @param collectionsTable the collections table + * @param prefixForCollectionTables the prefix for collection tables + * @param supportedKeyTypes the supported key types + * @param supportedDataTypes the supported data types + * @param supportedVectorTypes the supported vector types + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public JDBCVectorStoreQueryProvider( + @Nonnull DataSource dataSource, + @Nonnull String collectionsTable, + @Nonnull String prefixForCollectionTables, + @Nonnull Map, String> supportedKeyTypes, + @Nonnull Map, String> supportedDataTypes, + @Nonnull Map, String> supportedVectorTypes) { + this.dataSource = dataSource; + this.collectionsTable = collectionsTable; + this.prefixForCollectionTables = prefixForCollectionTables; + this.supportedKeyTypes = new HashMap<>(supportedKeyTypes); + this.supportedDataTypes = new HashMap<>(supportedDataTypes); + this.supportedVectorTypes = new HashMap<>(supportedVectorTypes); + } + + /** + * Creates a new builder. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Formats a wildcard string for a query. + * + * @param wildcards the number of wildcards + * @return the formatted wildcard string + */ + protected String getWildcardString(int wildcards) { + return Stream.generate(() -> "?") + .limit(wildcards) + .collect(Collectors.joining(", ")); + } + + /** + * Gets the key column name from a key field. + * + * @param keyField the key field + * @return the key column name + */ + protected String getKeyColumnName(VectorStoreRecordField keyField) { + return validateSQLidentifier(keyField.getEffectiveStorageName()); + } + + /** + * Formats the query columns from a record definition. + * + * @param fields the fields to get the columns from + * @return the formatted query columns + */ + protected String getQueryColumnsFromFields(List fields) { + return fields.stream() + .map(VectorStoreRecordField::getEffectiveStorageName) + .map(JDBCVectorStoreQueryProvider::validateSQLidentifier) + .collect(Collectors.joining(", ")); + } + + /** + * Formats the column names and types for a table. + * + * @param fields the fields + * @param types the types + * @return the formatted column names and types + */ + protected String getColumnNamesAndTypes(List fields, + Map, String> types) { + List columns = fields.stream() + .map(field -> validateSQLidentifier(field.getEffectiveStorageName()) + " " + + types.get(field.getFieldType())) + .collect(Collectors.toList()); + + return String.join(", ", columns); + } + + protected String getCollectionTableName(String collectionName) { + return validateSQLidentifier(prefixForCollectionTables + collectionName); + } + + /** + * Gets the supported key types and their corresponding SQL types. + * + * @return the supported key types + */ + @Override + public Map, String> getSupportedKeyTypes() { + return new HashMap<>(this.supportedKeyTypes); + } + + /** + * Gets the supported data types and their corresponding SQL types. + * + * @return the supported data types + */ + @Override + public Map, String> getSupportedDataTypes() { + return new HashMap<>(this.supportedDataTypes); + } + + /** + * Gets the supported vector types and their corresponding SQL types. + * + * @return the supported vector types + */ + @Override + public Map, String> getSupportedVectorTypes() { + return new HashMap<>(this.supportedVectorTypes); + } + + /** + * Prepares the vector store. Executes any necessary setup steps for the vector store. + * + * @throws SKException if an error occurs while preparing the vector store + */ + @Override + public void prepareVectorStore() { + String createCollectionsTable = formatQuery( + "CREATE TABLE IF NOT EXISTS %s (collectionId VARCHAR(255) PRIMARY KEY)", + validateSQLidentifier(collectionsTable)); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement createTable = connection.prepareStatement(createCollectionsTable)) { + createTable.execute(); + } catch (SQLException e) { + throw new SKException("Failed to prepare vector store", e); + } + } + + /** + * Checks if the types of the record class fields are supported. + * + * @param recordDefinition the record definition + * @throws SKException if the types are not supported + */ + @Override + public void validateSupportedTypes(VectorStoreRecordDefinition recordDefinition) { + + VectorStoreRecordDefinition.validateSupportedTypes( + Collections.singletonList(recordDefinition.getKeyField()), + getSupportedKeyTypes().keySet()); + VectorStoreRecordDefinition.validateSupportedTypes( + new ArrayList<>(recordDefinition.getDataFields()), + getSupportedDataTypes().keySet()); + VectorStoreRecordDefinition.validateSupportedTypes( + new ArrayList<>(recordDefinition.getVectorFields()), + getSupportedVectorTypes().keySet()); + } + + /** + * Checks if a collection exists. + * + * @param collectionName the collection name + * @return true if the collection exists, false otherwise + * @throws SKException if an error occurs while checking if the collection exists + */ + @Override + public boolean collectionExists(String collectionName) { + String query = formatQuery("SELECT 1 FROM %s WHERE collectionId = ?", + validateSQLidentifier(collectionsTable)); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + statement.setObject(1, collectionName); + + return statement.executeQuery().next(); + } catch (SQLException e) { + throw new SKException("Failed to check if collection exists", e); + } + } + + /** + * Creates a collection. + * + * @param collectionName the collection name + * @param recordDefinition the record definition + * @throws SKException if an error occurs while creating the collection + */ + @Override + @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") + @GuardedBy("dbCreationLock") + // SQL query is generated dynamically with valid identifiers + public void createCollection(String collectionName, + VectorStoreRecordDefinition recordDefinition) { + + synchronized (dbCreationLock) { + // No approximate search is supported in JDBCVectorStoreQueryProvider + if (recordDefinition.getVectorFields().stream() + .anyMatch( + field -> field.getIndexKind() != null && field.getIndexKind() != IndexKind.FLAT + && field.getIndexKind() != IndexKind.UNDEFINED)) { + LOGGER + .warn(String.format( + "Indexes are not supported in %s. Ignoring indexKind property.", + this.getClass().getName())); + } + + String createStorageTable = formatQuery("CREATE TABLE IF NOT EXISTS %s (" + + "%s VARCHAR(255) PRIMARY KEY, " + + "%s, " + + "%s);", + getCollectionTableName(collectionName), + getKeyColumnName(recordDefinition.getKeyField()), + getColumnNamesAndTypes(new ArrayList<>(recordDefinition.getDataFields()), + getSupportedDataTypes()), + getColumnNamesAndTypes(new ArrayList<>(recordDefinition.getVectorFields()), + getSupportedVectorTypes())); + + String insertCollectionQuery = this.getInsertCollectionQuery(collectionsTable); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement createTable = connection.prepareStatement(createStorageTable)) { + createTable.execute(); + } catch (SQLException e) { + throw new SKException("Failed to create collection", e); + } + + try (Connection connection = dataSource.getConnection(); + PreparedStatement insert = connection.prepareStatement(insertCollectionQuery)) { + insert.setObject(1, collectionName); + insert.execute(); + } catch (SQLException e) { + throw new SKException("Failed to insert collection", e); + } + } + } + + protected String getInsertCollectionQuery(String collectionsTable) { + return formatQuery( + "INSERT IGNORE INTO %s (collectionId) VALUES (?)", + validateSQLidentifier(collectionsTable)); + } + + /** + * Deletes a collection. + * + * @param collectionName the collection name + * @throws SKException if an error occurs while deleting the collection + */ + @Override + @GuardedBy("dbCreationLock") + public void deleteCollection(String collectionName) { + synchronized (dbCreationLock) { + String deleteCollectionOperation = formatQuery("DELETE FROM %s WHERE collectionId = ?", + validateSQLidentifier(collectionsTable)); + String dropTableOperation = formatQuery("DROP TABLE %s", + getCollectionTableName(collectionName)); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement deleteCollection = connection + .prepareStatement(deleteCollectionOperation)) { + deleteCollection.setObject(1, collectionName); + deleteCollection.execute(); + } catch (SQLException e) { + throw new SKException("Failed to delete collection", e); + } + + try (Connection connection = dataSource.getConnection(); + PreparedStatement dropTable = connection.prepareStatement(dropTableOperation)) { + dropTable.execute(); + } catch (SQLException e) { + throw new SKException("Failed to drop table", e); + } + } + } + + /** + * Gets the collection names. + * + * @return the collection names + * @throws SKException if an error occurs while getting the collection names + */ + @Override + public List getCollectionNames() { + String query = formatQuery("SELECT collectionId FROM %s", + validateSQLidentifier(collectionsTable)); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + List collectionNames = new ArrayList<>(); + ResultSet resultSet = statement.executeQuery(); + + while (resultSet.next()) { + collectionNames.add(resultSet.getString(1)); + } + + return Collections.unmodifiableList(collectionNames); + } catch (SQLException e) { + throw new SKException("Failed to get collection names", e); + } + } + + /** + * Gets a list of records from the store. + * + * @param collectionName the collection name + * @param keys the keys + * @param recordDefinition the record definition + * @param mapper the mapper + * @param options the options + * @param the record type + * @return the records + * @throws SKException if an error occurs while getting the records + */ + @Override + public List getRecords(String collectionName, List keys, + VectorStoreRecordDefinition recordDefinition, + VectorStoreRecordMapper mapper, + GetRecordOptions options) { + List fields; + if (options != null && options.isIncludeVectors()) { + fields = recordDefinition.getAllFields(); + } else { + fields = recordDefinition.getNonVectorFields(); + } + + String query; + + if (options != null && options.isWildcardKeyMatching()) { + if (keys.size() > 1) { + throw new SKException("If using wildcard key matching, only one key is allowed"); + } + query = "SELECT %s FROM %s WHERE %s LIKE (%s)"; + } else { + query = "SELECT %s FROM %s WHERE %s IN (%s)"; + } + + query = formatQuery(query, + getQueryColumnsFromFields(fields), + getCollectionTableName(collectionName), + getKeyColumnName(recordDefinition.getKeyField()), + getWildcardString(keys.size())); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + for (int i = 0; i < keys.size(); ++i) { + statement.setObject(i + 1, keys.get(i)); + } + + List records = new ArrayList<>(); + ResultSet resultSet = statement.executeQuery(); + + while (resultSet.next()) { + records.add(mapper.mapStorageModelToRecord(resultSet, options)); + } + + return Collections.unmodifiableList(records); + } catch (SQLException e) { + throw new SKException("Failed to set statement values", e); + } + } + + @Override + public void upsertRecords(String collectionName, List records, + VectorStoreRecordDefinition recordDefinition, UpsertRecordOptions options) { + throw new UnsupportedOperationException( + "Upsert is not supported. Try with a specific query provider."); + } + + /** + * Deletes records. + * + * @param collectionName the collection name + * @param keys the keys + * @param recordDefinition the record definition + * @param options the options + * @throws SKException if an error occurs while deleting the records + */ + @Override + public void deleteRecords(String collectionName, List keys, + VectorStoreRecordDefinition recordDefinition, DeleteRecordOptions options) { + String query = formatQuery("DELETE FROM %s WHERE %s IN (%s)", + getCollectionTableName(collectionName), + getKeyColumnName(recordDefinition.getKeyField()), + getWildcardString(keys.size())); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + for (int i = 0; i < keys.size(); ++i) { + statement.setObject(i + 1, keys.get(i)); + } + + statement.execute(); + } catch (SQLException e) { + throw new SKException("Failed to set statement values", e); + } + } + + protected List getRecordsWithFilter(String collectionName, + VectorStoreRecordDefinition recordDefinition, + VectorStoreRecordMapper mapper, + GetRecordOptions options, + String filter, + List parameters) { + List fields; + if (options.isIncludeVectors()) { + fields = recordDefinition.getAllFields(); + } else { + fields = recordDefinition.getNonVectorFields(); + } + + String filterClause = filter == null || filter.isEmpty() ? "" : "WHERE " + filter; + String selectQuery = formatQuery("SELECT %s FROM %s %s", + getQueryColumnsFromFields(fields), + getCollectionTableName(collectionName), + filterClause); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(selectQuery)) { + if (parameters != null) { + for (int i = 0; i < parameters.size(); ++i) { + statement.setObject(i + 1, parameters.get(i)); + } + } + + List records = new ArrayList<>(); + ResultSet resultSet = statement.executeQuery(); + while (resultSet.next()) { + records.add(mapper.mapStorageModelToRecord(resultSet, options)); + } + + return Collections.unmodifiableList(records); + } catch (SQLException e) { + throw new SKException("Failed to set statement values", e); + } + } + + /** + * Vector search. Executes a vector search query and returns the results. The results are mapped + * to the specified record type using the provided mapper. The query is executed against the + * specified collection. + * + * @param the record type + * @param collectionName the collection name + * @param vector the vector to search with + * @param options the search options + * @param recordDefinition the record definition + * @param mapper the mapper, responsible for mapping the result set to the record + * type. + * @return the search results + */ + @Override + public VectorSearchResults search(String collectionName, + List vector, VectorSearchOptions options, + VectorStoreRecordDefinition recordDefinition, + VectorStoreRecordMapper mapper) { + if (recordDefinition.getVectorFields().isEmpty()) { + throw new SKException("No vector fields defined. Cannot perform vector search"); + } + + VectorStoreRecordVectorField firstVectorField = recordDefinition.getVectorFields() + .get(0); + if (options == null) { + options = VectorSearchOptions.createDefault(firstVectorField.getName()); + } + + VectorStoreRecordVectorField vectorField = options.getVectorFieldName() == null + ? firstVectorField + : (VectorStoreRecordVectorField) recordDefinition + .getField(options.getVectorFieldName()); + + String filter = getFilter(options.getVectorSearchFilter(), recordDefinition); + List parameters = getFilterParameters(options.getVectorSearchFilter()); + + List records = getRecordsWithFilter(collectionName, recordDefinition, mapper, + new GetRecordOptions(true), filter, parameters); + + DistanceFunction distanceFunction = vectorField + .getDistanceFunction() == DistanceFunction.UNDEFINED + ? DistanceFunction.EUCLIDEAN_DISTANCE + : vectorField.getDistanceFunction(); + + return new VectorSearchResults<>( + VectorOperations.exactSimilaritySearch(records, vector, vectorField, + distanceFunction, options)); + } + + /** + * Validates an SQL identifier. + * + * @param identifier the identifier + * @return the identifier if it is valid + * @throws SKException if the identifier is invalid + */ + public static String validateSQLidentifier(String identifier) { + if (identifier.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { + return identifier; + } + throw new SKException("Invalid SQL identifier: " + identifier); + } + + /** + * Formats a query. + * + * @param query the query + * @param args the arguments + * @return the formatted query + */ + public String formatQuery(String query, String... args) { + return String.format(query, (Object[]) args); + } + + /** + * Gets the filter query string for the given vector search filter and record definition. + * + * @param filter The filter to get the filter string for. + * @param recordDefinition The record definition to get the filter string for. + * @return The filter string. + */ + @Override + public String getFilter(VectorSearchFilter filter, + VectorStoreRecordDefinition recordDefinition) { + if (filter == null + || filter.getFilterClauses().isEmpty()) { + return ""; + } + + return filter.getFilterClauses().stream().map(filterClause -> { + if (filterClause instanceof EqualToFilterClause) { + EqualToFilterClause equalToFilterClause = (EqualToFilterClause) filterClause; + return getEqualToFilter(new EqualToFilterClause( + recordDefinition.getField(equalToFilterClause.getFieldName()) + .getEffectiveStorageName(), + equalToFilterClause.getValue())); + } else if (filterClause instanceof AnyTagEqualToFilterClause) { + AnyTagEqualToFilterClause anyTagEqualToFilterClause = (AnyTagEqualToFilterClause) filterClause; + return getAnyTagEqualToFilter(new AnyTagEqualToFilterClause( + recordDefinition.getField(anyTagEqualToFilterClause.getFieldName()) + .getEffectiveStorageName(), + anyTagEqualToFilterClause.getValue())); + } else { + throw new SKException("Unsupported filter clause type '" + + filterClause.getClass().getSimpleName() + "'."); + } + }).collect(Collectors.joining(" AND ")); + } + + /** + * Gets the filter parameters for the given vector search filter to associate with the filter + * string generated by the getFilter method. + * + * @param filter The filter to get the filter parameters for. + * @return The filter parameters. + */ + @Override + public List getFilterParameters(VectorSearchFilter filter) { + if (filter == null + || filter.getFilterClauses().isEmpty()) { + return Collections.emptyList(); + } + + return filter.getFilterClauses().stream().map(filterClause -> { + if (filterClause instanceof EqualToFilterClause) { + EqualToFilterClause equalToFilterClause = (EqualToFilterClause) filterClause; + return equalToFilterClause.getValue(); + } else if (filterClause instanceof AnyTagEqualToFilterClause) { + AnyTagEqualToFilterClause anyTagEqualToFilterClause = (AnyTagEqualToFilterClause) filterClause; + return String.format("%%\"%s\"%%", anyTagEqualToFilterClause.getValue()); + } else { + throw new SKException("Unsupported filter clause type '" + + filterClause.getClass().getSimpleName() + "'."); + } + }).collect(Collectors.toList()); + } + + @Override + public String getEqualToFilter(EqualToFilterClause filterClause) { + String fieldName = JDBCVectorStoreQueryProvider + .validateSQLidentifier(filterClause.getFieldName()); + Object value = filterClause.getValue(); + + if (value instanceof String) { + return String.format("%s = ?", fieldName); + } else if (value instanceof Boolean) { + return String.format("%s = ?", fieldName); + } else if (value instanceof Integer) { + return String.format("%s = ?", fieldName); + } else if (value instanceof Long) { + return String.format("%s = ?", fieldName); + } else if (value instanceof Float) { + return String.format("%s = ?", fieldName); + } else if (value instanceof Double) { + return String.format("%s = ?", fieldName); + } else if (value instanceof OffsetDateTime) { + return String.format("%s = ?", fieldName); + } else { + throw new SKException("Unsupported filter value type '" + + value.getClass().getSimpleName() + "'."); + } + } + + @Override + public String getAnyTagEqualToFilter(AnyTagEqualToFilterClause filterClause) { + String fieldName = JDBCVectorStoreQueryProvider + .validateSQLidentifier(filterClause.getFieldName()); + + return String.format("%s LIKE ?", fieldName); + } + + @Override + public VectorStoreRecordMapper getVectorStoreRecordMapper( + Class recordClass, + VectorStoreRecordDefinition recordDefinition) { + return JDBCVectorStoreRecordMapper.builder() + .withRecordClass(recordClass) + .withVectorStoreRecordDefinition(recordDefinition) + .build(); + } + + /** + * The builder for {@link JDBCVectorStoreQueryProvider}. + */ + public static class Builder + implements SQLVectorStoreQueryProvider.Builder { + + private DataSource dataSource; + private String collectionsTable = DEFAULT_COLLECTIONS_TABLE; + private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; + + /** + * Sets the data source. + * + * @param dataSource the data source + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") // DataSource is not exposed + public Builder withDataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + /** + * Sets the collections table name. + * + * @param collectionsTable the collections table name + * @return the builder + */ + public Builder withCollectionsTable(String collectionsTable) { + this.collectionsTable = validateSQLidentifier(collectionsTable); + return this; + } + + /** + * Sets the prefix for collection tables. + * + * @param prefixForCollectionTables the prefix for collection tables + * @return the builder + */ + public Builder withPrefixForCollectionTables(String prefixForCollectionTables) { + this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); + return this; + } + + @Override + public JDBCVectorStoreQueryProvider build() { + if (dataSource == null) { + throw new SKException("DataSource is required"); + } + + return new JDBCVectorStoreQueryProvider(dataSource, collectionsTable, + prefixForCollectionTables); + } + } + +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollection.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollection.java similarity index 71% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollection.java rename to data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollection.java index b9c0bd3cc..fc421c2b0 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollection.java +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollection.java @@ -1,39 +1,51 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; +package com.microsoft.semantickernel.data.jdbc; import com.microsoft.semantickernel.builders.SemanticKernelBuilder; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.data.recordoptions.DeleteRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.options.DeleteRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; import com.microsoft.semantickernel.exceptions.SKException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; - -import javax.annotation.Nonnull; -import javax.sql.DataSource; import java.lang.reflect.Field; +import java.sql.ResultSet; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.sql.DataSource; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +/** + * The JDBCVectorStoreRecordCollection class represents a collection of records in a JDBC vector + * store. It implements the SQLVectorStoreRecordCollection interface and provides methods for + * managing the collection, such as creating, deleting, and upserting records. + * + * @param the type of the records in the collection + */ public class JDBCVectorStoreRecordCollection implements SQLVectorStoreRecordCollection { + private final String collectionName; - private final VectorStoreRecordDefinition recordDefinition; + protected final VectorStoreRecordDefinition recordDefinition; + protected final VectorStoreRecordMapper vectorStoreRecordMapper; private final JDBCVectorStoreRecordCollectionOptions options; - private final JDBCVectorStoreRecordMapper vectorStoreRecordMapper; - private final JDBCVectorStoreQueryProvider queryProvider; + protected final SQLVectorStoreQueryProvider queryProvider; /** * Creates a new instance of the {@link JDBCVectorStoreRecordCollection}. * - * @param dataSource the data source + * @param dataSource the data source * @param collectionName the name of the collection - * @param options the options + * @param options the options */ - @SuppressFBWarnings("EI_EXPOSE_REP2") // DataSource is not exposed public JDBCVectorStoreRecordCollection( @Nonnull DataSource dataSource, @Nonnull String collectionName, @@ -46,27 +58,26 @@ public JDBCVectorStoreRecordCollection( ? VectorStoreRecordDefinition.fromRecordClass(options.getRecordClass()) : options.getRecordDefinition(); - // If mapper is not provided, set a default one - if (options.getVectorStoreRecordMapper() == null) { - vectorStoreRecordMapper = JDBCVectorStoreRecordMapper.builder() - .withRecordClass(options.getRecordClass()) - .withVectorStoreRecordDefinition(recordDefinition) - .build(); - } else { - vectorStoreRecordMapper = options.getVectorStoreRecordMapper(); - } - // If the query provider is not provided, set a default one if (options.getQueryProvider() == null) { - this.queryProvider = JDBCVectorStoreDefaultQueryProvider.builder() + this.queryProvider = JDBCVectorStoreQueryProvider.builder() .withDataSource(dataSource) .build(); } else { this.queryProvider = options.getQueryProvider(); } + // If mapper is not provided, set a default one + if (options.getVectorStoreRecordMapper() == null) { + vectorStoreRecordMapper = options.getQueryProvider() + .getVectorStoreRecordMapper(options.getRecordClass(), + recordDefinition); + } else { + vectorStoreRecordMapper = options.getVectorStoreRecordMapper(); + } + // Check if the types are supported - queryProvider.validateSupportedTypes(options.getRecordClass(), recordDefinition); + queryProvider.validateSupportedTypes(recordDefinition); } /** @@ -99,12 +110,11 @@ public Mono collectionExistsAsync() { * @throws SKException if the operation fails */ @Override - public Mono createCollectionAsync() { + public Mono> createCollectionAsync() { return Mono.fromRunnable( - () -> queryProvider.createCollection(this.collectionName, options.getRecordClass(), - recordDefinition)) + () -> queryProvider.createCollection(this.collectionName, recordDefinition)) .subscribeOn(Schedulers.boundedElastic()) - .then(); + .then(Mono.just(this)); } /** @@ -114,7 +124,7 @@ public Mono createCollectionAsync() { * @throws SKException if the operation fails */ @Override - public Mono createCollectionIfNotExistsAsync() { + public Mono> createCollectionIfNotExistsAsync() { return collectionExistsAsync().map( exists -> { if (!exists) { @@ -123,7 +133,7 @@ public Mono createCollectionIfNotExistsAsync() { return Mono.empty(); }) .flatMap(mono -> mono) - .then(); + .then(Mono.just(this)); } /** @@ -143,13 +153,15 @@ public Mono deleteCollectionAsync() { /** * Gets a record from the store. * - * @param key The key of the record to get. + * @param key The key of the record to get. * @param options The options for getting the record. * @return A Mono emitting the record. * @throws SKException if the operation fails */ @Override public Mono getAsync(String key, GetRecordOptions options) { + Objects.requireNonNull(key, "key is required"); + return this.getBatchAsync(Collections.singletonList(key), options) .mapNotNull(records -> { if (records.isEmpty()) { @@ -162,18 +174,19 @@ public Mono getAsync(String key, GetRecordOptions options) { /** * Gets a batch of records from the store. * - * @param keys The keys of the records to get. + * @param keys The keys of the records to get. * @param options The options for getting the records. * @return A Mono emitting a collection of records. * @throws SKException if the operation fails */ @Override - public Mono> getBatchAsync(List keys, GetRecordOptions options) { + public Mono> getBatchAsync(@Nonnull List keys, GetRecordOptions options) { + Objects.requireNonNull(keys, "keys is required"); + return Mono.fromCallable( - () -> { - return queryProvider.getRecords(this.collectionName, keys, recordDefinition, - vectorStoreRecordMapper, options); - }).subscribeOn(Schedulers.boundedElastic()); + () -> queryProvider.getRecords(this.collectionName, keys, recordDefinition, + vectorStoreRecordMapper, options)) + .subscribeOn(Schedulers.boundedElastic()); } protected String getKeyFromRecord(Record data) { @@ -181,7 +194,7 @@ protected String getKeyFromRecord(Record data) { Field keyField = data.getClass() .getDeclaredField(recordDefinition.getKeyField().getName()); keyField.setAccessible(true); - return (String) keyField.get(data); + return keyField.get(data).toString(); } catch (NoSuchFieldException | IllegalAccessException e) { throw new SKException("Failed to get key from record", e); } @@ -197,6 +210,8 @@ protected String getKeyFromRecord(Record data) { */ @Override public Mono upsertAsync(Record data, UpsertRecordOptions options) { + Objects.requireNonNull(data, "data is required"); + return this.upsertBatchAsync(Collections.singletonList(data), options) .mapNotNull(keys -> { if (keys.isEmpty()) { @@ -216,6 +231,8 @@ public Mono upsertAsync(Record data, UpsertRecordOptions options) { */ @Override public Mono> upsertBatchAsync(List data, UpsertRecordOptions options) { + Objects.requireNonNull(data, "data is required"); + return Mono.fromCallable( () -> { queryProvider.upsertRecords(this.collectionName, data, recordDefinition, options); @@ -227,7 +244,7 @@ public Mono> upsertBatchAsync(List data, UpsertRecordOption /** * Deletes a record from the store. * - * @param key The key of the record to delete. + * @param key The key of the record to delete. * @param options The options for deleting the record. * @return A Mono representing the completion of the deletion operation. * @throws SKException if the operation fails @@ -240,7 +257,7 @@ public Mono deleteAsync(String key, DeleteRecordOptions options) { /** * Deletes a batch of records from the store. * - * @param keys The keys of the records to delete. + * @param keys The keys of the records to delete. * @param options The options for deleting the records. * @return A Mono representing the completion of the deletion operation. * @throws SKException if the operation fails @@ -265,8 +282,31 @@ public Mono prepareAsync() { .subscribeOn(Schedulers.boundedElastic()).then(); } + /** + * Vectorized search. This method searches for records that are similar to the given vector. + * + * @param vector The vector to search with. + * @param vectorSearchOptions The options to use for the search. + * @return A list of search results. + */ + @Override + public Mono> searchAsync(List vector, + VectorSearchOptions vectorSearchOptions) { + return Mono.fromCallable( + () -> queryProvider.search(this.collectionName, vector, vectorSearchOptions, + recordDefinition, + vectorStoreRecordMapper)) + .subscribeOn(Schedulers.boundedElastic()); + } + + /** + * Builder for a JDBCVectorStoreRecordCollection. + * + * @param the type of the records in the collection + */ public static class Builder implements SemanticKernelBuilder> { + private DataSource dataSource; private String collectionName; private JDBCVectorStoreRecordCollectionOptions options; @@ -308,13 +348,13 @@ public Builder withOptions(JDBCVectorStoreRecordCollectionOptions build() { if (dataSource == null) { - throw new IllegalArgumentException("dataSource is required"); + throw new SKException("dataSource is required"); } if (collectionName == null) { - throw new IllegalArgumentException("collectionName is required"); + throw new SKException("collectionName is required"); } if (options == null) { - throw new IllegalArgumentException("options is required"); + throw new SKException("options is required"); } return new JDBCVectorStoreRecordCollection<>(dataSource, collectionName, options); diff --git a/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollectionFactory.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollectionFactory.java new file mode 100644 index 000000000..796f0720c --- /dev/null +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollectionFactory.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc; + +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; + +import javax.sql.DataSource; + +/** + * Factory for creating JDBC vector store record collections. + */ +public interface JDBCVectorStoreRecordCollectionFactory { + + /** + * Creates a new JDBC vector store record collection. + * + * @param dataSource The JDBC data source. + * @param collectionName The name of the collection. + * @param recordClass The class type of the + * @param recordDefinition The record definition. + * @param The type of record in the collection. + * @return The new JDBC vector store record collection. + */ + JDBCVectorStoreRecordCollection createVectorStoreRecordCollection( + DataSource dataSource, + String collectionName, + Class recordClass, + VectorStoreRecordDefinition recordDefinition); +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollectionOptions.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollectionOptions.java similarity index 68% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollectionOptions.java rename to data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollectionOptions.java index af1ec49ec..596d0f298 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollectionOptions.java +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordCollectionOptions.java @@ -1,26 +1,35 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; +package com.microsoft.semantickernel.data.jdbc; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.exceptions.SKException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import static com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreDefaultQueryProvider.validateSQLidentifier; -import static com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreQueryProvider.DEFAULT_COLLECTIONS_TABLE; -import static com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreQueryProvider.DEFAULT_PREFIX_FOR_COLLECTION_TABLES; +import java.sql.ResultSet; -public class JDBCVectorStoreRecordCollectionOptions { +import static com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider.DEFAULT_COLLECTIONS_TABLE; +import static com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider.DEFAULT_PREFIX_FOR_COLLECTION_TABLES; + +/** + * Options for a JDBC vector store record collection. + * @param the record type + */ +public class JDBCVectorStoreRecordCollectionOptions + implements VectorStoreRecordCollectionOptions { private final Class recordClass; - private final JDBCVectorStoreRecordMapper vectorStoreRecordMapper; + private final VectorStoreRecordMapper vectorStoreRecordMapper; private final VectorStoreRecordDefinition recordDefinition; - private final JDBCVectorStoreQueryProvider queryProvider; + private final SQLVectorStoreQueryProvider queryProvider; private final String collectionsTableName; private final String prefixForCollectionTables; private JDBCVectorStoreRecordCollectionOptions( Class recordClass, VectorStoreRecordDefinition recordDefinition, - JDBCVectorStoreRecordMapper vectorStoreRecordMapper, - JDBCVectorStoreQueryProvider queryProvider, + VectorStoreRecordMapper vectorStoreRecordMapper, + SQLVectorStoreQueryProvider queryProvider, String collectionsTableName, String prefixForCollectionTables) { this.recordClass = recordClass; @@ -40,6 +49,16 @@ public static Builder builder() { return new Builder<>(); } + /** + * Gets the key class. + * + * @return the key class + */ + @Override + public Class getKeyClass() { + return String.class; + } + /** * Gets the record class. * @return the record class @@ -60,7 +79,7 @@ public VectorStoreRecordDefinition getRecordDefinition() { * Gets the vector store record mapper. * @return the vector store record mapper */ - public JDBCVectorStoreRecordMapper getVectorStoreRecordMapper() { + public VectorStoreRecordMapper getVectorStoreRecordMapper() { return vectorStoreRecordMapper; } @@ -85,15 +104,19 @@ public String getPrefixForCollectionTables() { * @return the query provider */ @SuppressFBWarnings("EI_EXPOSE_REP") // DataSource in queryProvider is not exposed - public JDBCVectorStoreQueryProvider getQueryProvider() { + public SQLVectorStoreQueryProvider getQueryProvider() { return queryProvider; } + /** + * Builder for JDBC vector store record collection options. + * @param the record type + */ public static class Builder { private Class recordClass; private VectorStoreRecordDefinition recordDefinition; - private JDBCVectorStoreRecordMapper vectorStoreRecordMapper; - private JDBCVectorStoreQueryProvider queryProvider; + private VectorStoreRecordMapper vectorStoreRecordMapper; + private SQLVectorStoreQueryProvider queryProvider; private String collectionsTableName = DEFAULT_COLLECTIONS_TABLE; private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; @@ -123,7 +146,7 @@ public Builder withRecordDefinition(VectorStoreRecordDefinition recordDe * @return the builder */ public Builder withVectorStoreRecordMapper( - JDBCVectorStoreRecordMapper vectorStoreRecordMapper) { + VectorStoreRecordMapper vectorStoreRecordMapper) { this.vectorStoreRecordMapper = vectorStoreRecordMapper; return this; } @@ -134,7 +157,7 @@ public Builder withVectorStoreRecordMapper( * @return the builder */ @SuppressFBWarnings("EI_EXPOSE_REP2") // DataSource in queryProvider is not exposed - public Builder withQueryProvider(JDBCVectorStoreQueryProvider queryProvider) { + public Builder withQueryProvider(SQLVectorStoreQueryProvider queryProvider) { this.queryProvider = queryProvider; return this; } @@ -145,7 +168,8 @@ public Builder withQueryProvider(JDBCVectorStoreQueryProvider queryProvi * @return the builder */ public Builder withCollectionsTableName(String collectionsTableName) { - this.collectionsTableName = validateSQLidentifier(collectionsTableName); + this.collectionsTableName = JDBCVectorStoreQueryProvider + .validateSQLidentifier(collectionsTableName); return this; } @@ -155,7 +179,8 @@ public Builder withCollectionsTableName(String collectionsTableName) { * @return the builder */ public Builder withPrefixForCollectionTables(String prefixForCollectionTables) { - this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); + this.prefixForCollectionTables = JDBCVectorStoreQueryProvider + .validateSQLidentifier(prefixForCollectionTables); return this; } @@ -165,7 +190,7 @@ public Builder withPrefixForCollectionTables(String prefixForCollectionT */ public JDBCVectorStoreRecordCollectionOptions build() { if (recordClass == null) { - throw new IllegalArgumentException("recordClass is required"); + throw new SKException("recordClass is required"); } return new JDBCVectorStoreRecordCollectionOptions<>( diff --git a/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordMapper.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordMapper.java new file mode 100644 index 000000000..b52071cf8 --- /dev/null +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/JDBCVectorStoreRecordMapper.java @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import java.util.List; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.function.BiFunction; + +/** + * Maps a JDBC result set to a record. + * + * @param the record type + */ +public class JDBCVectorStoreRecordMapper + extends VectorStoreRecordMapper { + + /** + * Constructs a new instance of the VectorStoreRecordMapper. + * + * @param storageModelToRecordMapper the function to convert a storage model to a record + */ + protected JDBCVectorStoreRecordMapper( + BiFunction storageModelToRecordMapper) { + super(null, storageModelToRecordMapper); + } + + /** + * Creates a new builder. + * + * @param the record type + * @return the builder + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * Operation not supported. + */ + @Override + public ResultSet mapRecordToStorageModel(Record record) { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Builder for {@link JDBCVectorStoreRecordMapper}. + * + * @param the record type + */ + public static class Builder + implements SemanticKernelBuilder> { + private Class recordClass; + private VectorStoreRecordDefinition vectorStoreRecordDefinition; + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Sets the record class. + * + * @param recordClass the record class + * @return the builder + */ + public Builder withRecordClass(Class recordClass) { + this.recordClass = recordClass; + return this; + } + + /** + * Sets the vector store record definition. + * + * @param vectorStoreRecordDefinition the vector store record definition + * @return the builder + */ + public Builder withVectorStoreRecordDefinition( + VectorStoreRecordDefinition vectorStoreRecordDefinition) { + this.vectorStoreRecordDefinition = vectorStoreRecordDefinition; + return this; + } + + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Builds the {@link JDBCVectorStoreRecordMapper}. + * + * @return the {@link JDBCVectorStoreRecordMapper} + */ + public JDBCVectorStoreRecordMapper build() { + if (recordClass == null) { + throw new SKException("recordClass is required"); + } + if (vectorStoreRecordDefinition == null) { + throw new SKException("vectorStoreRecordDefinition is required"); + } + + return new JDBCVectorStoreRecordMapper<>( + (resultSet, options) -> { + try { + // Create an ObjectNode to hold the values + ObjectNode objectNode = objectMapper.createObjectNode(); + + // Select fields from the record definition. + List fields; + if (options != null && options.isIncludeVectors()) { + fields = vectorStoreRecordDefinition.getAllFields(); + } else { + fields = vectorStoreRecordDefinition.getNonVectorFields(); + } + + for (VectorStoreRecordField field : fields) { + Object value = resultSet.getObject(field.getEffectiveStorageName()); + Class fieldType = field.getFieldType(); + + if (field instanceof VectorStoreRecordVectorField) { + // If the vector field is other than String, deserialize it from the JSON string + if (!fieldType.equals(String.class)) { + value = objectMapper.readValue((String) value, fieldType); + } + } else if (field instanceof VectorStoreRecordDataField) { + // If the field is List, deserialize it from the JSON string + if (fieldType.equals(List.class)) { + value = objectMapper.readValue((String) value, fieldType); + } + } + + JsonNode genericNode = objectMapper.valueToTree(value); + objectNode.set(field.getEffectiveStorageName(), genericNode); + } + + // Deserialize the object node to the record class + return objectMapper.convertValue(objectNode, recordClass); + } catch (SQLException | JsonProcessingException e) { + throw new SKException( + "Failure to serialize object, by default the JDBC connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", + e); + } + }); + } + } +} diff --git a/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStore.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStore.java new file mode 100644 index 000000000..08d167b8f --- /dev/null +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStore.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc; + +import com.microsoft.semantickernel.data.vectorstorage.VectorStore; +import reactor.core.publisher.Mono; + +/** + * Represents a SQL vector store. + */ +public interface SQLVectorStore + extends VectorStore { + + /** + * Prepares the vector store. + * + * @return A {@link Mono} that completes when the vector store is prepared to be used. + */ + Mono prepareAsync(); +} diff --git a/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreFilterQueryProvider.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreFilterQueryProvider.java new file mode 100644 index 000000000..4feb272f3 --- /dev/null +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreFilterQueryProvider.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc; + +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.filter.FilterMapping; + +import java.util.List; + +public interface SQLVectorStoreFilterQueryProvider extends FilterMapping { + /** + * Gets the filter parameters for the given vector search filter to associate with the filter string + * generated by the getFilter method. + * + * @param filter The filter to get the filter parameters for. + * @return The filter parameters. + */ + List getFilterParameters(VectorSearchFilter filter); +} diff --git a/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreQueryProvider.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreQueryProvider.java new file mode 100644 index 000000000..30a535fb0 --- /dev/null +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreQueryProvider.java @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc; + +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.options.DeleteRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; + +import java.sql.ResultSet; +import java.util.List; +import java.util.Map; + +/** + * The JDBC vector store query provider. + * Provides the necessary methods to interact with a JDBC vector store and vector store collections. + */ +public interface SQLVectorStoreQueryProvider { + /** + * The default name for the collections table. + */ + String DEFAULT_COLLECTIONS_TABLE = "SKCollections"; + + /** + * The prefix for collection tables. + */ + String DEFAULT_PREFIX_FOR_COLLECTION_TABLES = "SKCollection_"; + + /** + * Gets the supported key types and their corresponding SQL types. + * + * @return the supported key types + */ + Map, String> getSupportedKeyTypes(); + + /** + * Gets the supported data types and their corresponding SQL types. + * + * @return the supported data types + */ + Map, String> getSupportedDataTypes(); + + /** + * Gets the supported vector types and their corresponding SQL types. + * + * @return the supported vector types + */ + Map, String> getSupportedVectorTypes(); + + /** + * Prepares the vector store. + * Executes any necessary setup steps for the vector store. + */ + void prepareVectorStore(); + + /** + * Checks if the types of the record class fields are supported. + * + * @param recordDefinition the record definition + */ + void validateSupportedTypes(VectorStoreRecordDefinition recordDefinition); + + /** + * Checks if a collection exists. + * + * @param collectionName the collection name + * @return true if the collection exists, false otherwise + */ + boolean collectionExists(String collectionName); + + /** + * Creates a collection. + * + * @param collectionName the collection name + * @param recordDefinition the record definition + */ + void createCollection(String collectionName, VectorStoreRecordDefinition recordDefinition); + + /** + * Deletes a collection. + * + * @param collectionName the collection name + */ + void deleteCollection(String collectionName); + + /** + * Gets the collection names. + * + * @return the collection names + */ + List getCollectionNames(); + + /** + * Gets records. + * + * @param collectionName the collection name + * @param keys the keys + * @param recordDefinition the record definition + * @param mapper the mapper + * @param options the options + * @param the record type + * @return the records + */ + List getRecords(String collectionName, List keys, + VectorStoreRecordDefinition recordDefinition, + VectorStoreRecordMapper mapper, + GetRecordOptions options); + + /** + * Upserts records. + * + * @param collectionName the collection name + * @param records the records + * @param vectorStoreRecordDefinition the record definition + * @param options the options + */ + void upsertRecords(String collectionName, List records, + VectorStoreRecordDefinition vectorStoreRecordDefinition, UpsertRecordOptions options); + + /** + * Deletes records. + * + * @param collectionName the collection name + * @param keys the keys + * @param recordDefinition the record definition + * @param options the options + */ + void deleteRecords(String collectionName, List keys, + VectorStoreRecordDefinition recordDefinition, DeleteRecordOptions options); + + /** + * Vector search. + * Executes a vector search query and returns the results. + * The results are mapped to the specified record type using the provided mapper. + * The query is executed against the specified collection. + * + * @param the record type + * @param collectionName the collection name + * @param vector the vector to search with + * @param options the vector search options + * @param recordDefinition the record definition + * @param mapper the mapper, responsible for mapping the result set to the record type. + * @return the search results + */ + VectorSearchResults search(String collectionName, + List vector, + VectorSearchOptions options, + VectorStoreRecordDefinition recordDefinition, + VectorStoreRecordMapper mapper); + + /** + * Gets the record mapper for the given record class and definition. + * + * @param the record type + * @param recordClass the record class + * @param recordDefinition the record definition + * @return the record mapper that maps JDBC result sets to the given record. + */ + VectorStoreRecordMapper getVectorStoreRecordMapper( + final Class recordClass, + final VectorStoreRecordDefinition recordDefinition); + + /** + * The builder for the JDBC vector store query provider. + */ + interface Builder extends SemanticKernelBuilder { + + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/SQLVectorStoreRecordCollection.java b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreRecordCollection.java similarity index 61% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/SQLVectorStoreRecordCollection.java rename to data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreRecordCollection.java index ff12c88b6..03fe3e074 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/SQLVectorStoreRecordCollection.java +++ b/data/semantickernel-data-jdbc/src/main/java/com/microsoft/semantickernel/data/jdbc/SQLVectorStoreRecordCollection.java @@ -1,9 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; +package com.microsoft.semantickernel.data.jdbc; -import com.microsoft.semantickernel.data.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; import reactor.core.publisher.Mono; +/** + * Represents a SQL vector store record collection. + * + * @param the key type + * @param the record type + */ public interface SQLVectorStoreRecordCollection extends VectorStoreRecordCollection { diff --git a/data/semantickernel-data-mysql/pom.xml b/data/semantickernel-data-mysql/pom.xml new file mode 100644 index 000000000..2ad3ec2de --- /dev/null +++ b/data/semantickernel-data-mysql/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../../pom.xml + + + com.microsoft.semantic-kernel + semantickernel-data-mysql + Semantic Kernel MySQL connector + Provides a MySQL connector for the Semantic Kernel + + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-data-jdbc + + + com.microsoft.semantic-kernel + semantickernel-api-data + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + + com.fasterxml.jackson.core + jackson-databind + compile + + + com.fasterxml.jackson.core + jackson-core + compile + + + com.github.spotbugs + spotbugs-annotations + + + \ No newline at end of file diff --git a/data/semantickernel-data-mysql/src/main/java/com/microsoft/semantickernel/data/jdbc/mysql/MySQLVectorStoreQueryProvider.java b/data/semantickernel-data-mysql/src/main/java/com/microsoft/semantickernel/data/jdbc/mysql/MySQLVectorStoreQueryProvider.java new file mode 100644 index 000000000..1ff0f7068 --- /dev/null +++ b/data/semantickernel-data-mysql/src/main/java/com/microsoft/semantickernel/data/jdbc/mysql/MySQLVectorStoreQueryProvider.java @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc.mysql; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.sql.DataSource; + +/** + * The MySQL vector store query provider. + * Provides the necessary methods to interact with a MySQL + * vector store and vector store collections. + */ +public class MySQLVectorStoreQueryProvider extends + JDBCVectorStoreQueryProvider implements SQLVectorStoreQueryProvider { + + private final ObjectMapper objectMapper; + + private MySQLVectorStoreQueryProvider( + @Nonnull DataSource dataSource, + @Nonnull String collectionsTable, + @Nonnull String prefixForCollectionTables, + @Nonnull ObjectMapper objectMapper) { + super(dataSource, collectionsTable, prefixForCollectionTables); + this.objectMapper = objectMapper; + } + + /** + * Creates a new builder. + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + private void setUpsertStatementValues(PreparedStatement statement, Object record, + List fields) { + JsonNode jsonNode = objectMapper.valueToTree(record); + + for (int i = 0; i < fields.size(); ++i) { + VectorStoreRecordField field = fields.get(i); + try { + JsonNode valueNode = jsonNode.get(field.getEffectiveStorageName()); + + if (field instanceof VectorStoreRecordVectorField) { + // Convert the vector field to a string + if (!field.getFieldType().equals(String.class)) { + statement.setObject(i + 1, objectMapper.writeValueAsString(valueNode)); + continue; + } + } else if (field instanceof VectorStoreRecordDataField) { + // Convert List field to a string + if (field.getFieldType().equals(List.class)) { + statement.setObject(i + 1, objectMapper.writeValueAsString(valueNode)); + continue; + } + } + + statement.setObject(i + 1, + objectMapper.convertValue(valueNode, field.getFieldType())); + } catch (SQLException | JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Upserts records into the collection. + * @param collectionName the collection name + * @param records the records to upsert + * @param recordDefinition the record definition + * @param options the upsert options + * @throws SKException if the upsert fails + */ + @Override + @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") // SQL query is generated dynamically with valid identifiers + public void upsertRecords(String collectionName, List records, + VectorStoreRecordDefinition recordDefinition, UpsertRecordOptions options) { + List fields = recordDefinition.getAllFields(); + + String onDuplicateKeyUpdate = fields.stream() + .map(field -> formatQuery("%s = VALUES(%s)", + validateSQLidentifier(field.getEffectiveStorageName()), + field.getEffectiveStorageName())) + .collect(Collectors.joining(", ")); + + String query = formatQuery("INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s", + getCollectionTableName(collectionName), + getQueryColumnsFromFields(fields), + getWildcardString(fields.size()), + onDuplicateKeyUpdate); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + for (Object record : records) { + setUpsertStatementValues(statement, record, recordDefinition.getAllFields()); + statement.addBatch(); + } + + statement.executeBatch(); + } catch (SQLException e) { + throw new SKException("Failed to upsert records", e); + } + } + + /** + * The MySQL vector store query provider builder. + */ + public static class Builder + extends JDBCVectorStoreQueryProvider.Builder { + private DataSource dataSource; + private String collectionsTable = DEFAULT_COLLECTIONS_TABLE; + private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; + private ObjectMapper objectMapper = new ObjectMapper(); + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withDataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + /** + * Sets the collections table name. + * @param collectionsTable the collections table name + * @return the builder + */ + public Builder withCollectionsTable(String collectionsTable) { + this.collectionsTable = validateSQLidentifier(collectionsTable); + return this; + } + + /** + * Sets the prefix for collection tables. + * @param prefixForCollectionTables the prefix for collection tables + * @return the builder + */ + public Builder withPrefixForCollectionTables(String prefixForCollectionTables) { + this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); + return this; + } + + /** + * Sets the object mapper. + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + public MySQLVectorStoreQueryProvider build() { + if (dataSource == null) { + throw new SKException("DataSource is required"); + } + + return new MySQLVectorStoreQueryProvider(dataSource, collectionsTable, + prefixForCollectionTables, objectMapper); + } + } +} diff --git a/data/semantickernel-data-oracle/pom.xml b/data/semantickernel-data-oracle/pom.xml new file mode 100644 index 000000000..42ec5b2cd --- /dev/null +++ b/data/semantickernel-data-oracle/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../../pom.xml + + + semantickernel-data-oracle + Semantic Kernel Oracle connector + Provides a Oracle connector for the Semantic Kernel + + + 1.21.4 + + + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + + + + com.microsoft.semantic-kernel + semantickernel-api-data + provided + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + provided + + + com.microsoft.semantic-kernel + semantickernel-api-builders + provided + + + com.microsoft.semantic-kernel + semantickernel-data-jdbc + + + com.fasterxml.jackson.core + jackson-databind + compile + + + com.fasterxml.jackson.core + jackson-core + compile + + + com.oracle.database.jdbc + ojdbc11 + 23.26.1.0.0 + + + com.oracle.database.jdbc + ojdbc-provider-jackson-oson + 1.0.6 + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.testcontainers + testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + oracle-free + test + + + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + android + test + + check + + + + + true + + + + com.diffplug.spotless + spotless-maven-plugin + + true + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${skipTests.oracle} + + + + + \ No newline at end of file diff --git a/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleDataTypesMapping.java b/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleDataTypesMapping.java new file mode 100644 index 000000000..1d5db527f --- /dev/null +++ b/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleDataTypesMapping.java @@ -0,0 +1,93 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +/** + * Defines oracle database type constants for supported java types. + */ +public class OracleDataTypesMapping { + + /** + * Oracle database type used when strings are mapped to VARCHAR + */ + public static final String STRING_VARCHAR = "VARCHAR2(%s)"; + /** + * Oracle database type used when strings are mapped to CLOB + */ + public static final String STRING_CLOB = "CLOB"; + /** + * Oracle database type used to map booleans + */ + public static final String BOOLEAN = "BOOLEAN"; + /** + * Oracle database type used to map bytes + */ + public static final String BYTE = "NUMBER(3)"; + /** + * Oracle database type used to map byte arrays + */ + public static final String BYTE_ARRAY = "RAW(2000)"; + /** + * Oracle database type used to map shorts + */ + public static final String SHORT = "NUMBER(5)"; + /** + * Oracle database type used to map ints + */ + public static final String INTEGER = "NUMBER(10)"; + /** + * Oracle database type used to map longs + */ + public static final String LONG = "NUMBER(19)"; + /** + * Oracle database type used to map float + */ + public static final String FLOAT = "BINARY_FLOAT"; + /** + * Oracle database type used to map double + */ + public static final String DOUBLE = "BINARY_DOUBLE"; + /** + * Oracle database type used to map BigDecimal + */ + public static final String DECIMAL = "NUMBER"; + /** + * Oracle database type used to map offset date time + */ + public static final String OFFSET_DATE_TIME = "TIMESTAMP(9) WITH TIME ZONE"; + /** + * Oracle database type used to map UUID + */ + public static final String UUID = "VARCHAR2(36)"; + /** + * Oracle database type used to map lists + */ + public static final String JSON = "JSON"; + /** + * Oracle database type used to map vectors (the parameter is the dimension of the vector) + */ + public static final String VECTOR_FLOAT = "VECTOR(%s, FLOAT32)"; +} diff --git a/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreFieldHelper.java b/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreFieldHelper.java new file mode 100644 index 000000000..bf4dd2952 --- /dev/null +++ b/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreFieldHelper.java @@ -0,0 +1,287 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.microsoft.semantickernel.data.jdbc.oracle.OracleVectorStoreQueryProvider.StringTypeMapping; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.exceptions.SKException; +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Helper class for field operations. Handles mapping between field java types to DB types and + * generating SQL statement to create field indexes. + */ +class OracleVectorStoreFieldHelper { + + /** + * Object naming regular expression + */ + private static final String OBJECT_NAMING_REGEXP = "[a-zA-Z_][a-zA-Z0-9_]{1,128}"; + /** + * The logger + */ + private static final Logger LOGGER = Logger.getLogger(OracleVectorStoreFieldHelper.class.getName()); + + /** + * Maps supported key java classes to Oracle database types + */ + private static final HashMap, String> supportedKeyTypes = new HashMap(); + static { + supportedKeyTypes.put(String.class, String.format(OracleDataTypesMapping.STRING_VARCHAR, 255)); + } + + /** + * Maps supported vector java classes to Oracle database types + */ + private static final Map, String> supportedVectorTypes = new HashMap(); + static { + supportedVectorTypes.put(String.class, OracleDataTypesMapping.VECTOR_FLOAT); + supportedVectorTypes.put(List.class, OracleDataTypesMapping.VECTOR_FLOAT); + supportedVectorTypes.put(Collection.class, OracleDataTypesMapping.VECTOR_FLOAT); + supportedVectorTypes.put(float[].class, OracleDataTypesMapping.VECTOR_FLOAT); + supportedVectorTypes.put(Float[].class, OracleDataTypesMapping.VECTOR_FLOAT); + } + + /** + * Maps supported data java classes to Oracle database types + */ + private static final HashMap, String> supportedDataTypes = new HashMap(); + static { + supportedDataTypes.put(byte.class, OracleDataTypesMapping.BYTE); + supportedDataTypes.put(Byte.class, OracleDataTypesMapping.BYTE); + supportedDataTypes.put(short.class, OracleDataTypesMapping.SHORT); + supportedDataTypes.put(Short.class, OracleDataTypesMapping.SHORT); + supportedDataTypes.put(int.class, OracleDataTypesMapping.INTEGER); + supportedDataTypes.put(Integer.class, OracleDataTypesMapping.INTEGER); + supportedDataTypes.put(long.class, OracleDataTypesMapping.LONG); + supportedDataTypes.put(Long.class, OracleDataTypesMapping.LONG); + supportedDataTypes.put(Float.class, OracleDataTypesMapping.FLOAT); + supportedDataTypes.put(float.class, OracleDataTypesMapping.FLOAT); + supportedDataTypes.put(Double.class, OracleDataTypesMapping.DOUBLE); + supportedDataTypes.put(double.class, OracleDataTypesMapping.DOUBLE); + supportedDataTypes.put(BigDecimal.class, OracleDataTypesMapping.DECIMAL); + supportedDataTypes.put(Boolean.class, OracleDataTypesMapping.BOOLEAN); + supportedDataTypes.put(boolean.class, OracleDataTypesMapping.BOOLEAN); + supportedDataTypes.put(OffsetDateTime.class, OracleDataTypesMapping.OFFSET_DATE_TIME); + supportedDataTypes.put(UUID.class, OracleDataTypesMapping.UUID); + supportedDataTypes.put(byte[].class, OracleDataTypesMapping.BYTE_ARRAY); + supportedDataTypes.put(List.class, OracleDataTypesMapping.JSON); + } + + /** + * Suffix added to the effective column name to generate the index name for a vector column. + */ + public static final String VECTOR_INDEX_SUFFIX = "_VECTOR_INDEX"; + + /** + * Gets the mapping between the supported Java key types and the Oracle database type. + * + * @return the mapping between the supported Java key types and the Oracle database type. + */ + static Map, String> getSupportedKeyTypes() { + + return Collections.unmodifiableMap(supportedKeyTypes); + } + + /** + * Gets the mapping between the supported Java data types and the Oracle database type. + * + * @return the mapping between the supported Java data types and the Oracle database type. + */ + static Map, String> getSupportedDataTypes( + StringTypeMapping stringTypeMapping, int defaultVarCharLength) { + String stringType = stringTypeMapping.equals(StringTypeMapping.USE_VARCHAR) + ? String.format(OracleDataTypesMapping.STRING_VARCHAR, defaultVarCharLength) + : OracleDataTypesMapping.STRING_CLOB; + supportedDataTypes.put(String.class, stringType); + LOGGER.finest("Mapping String columns to " + stringType); + return Collections.unmodifiableMap(supportedDataTypes); + } + + /** + * Gets the mapping between the supported Java data types and the Oracle database type. + * + * @return the mapping between the supported Java data types and the Oracle database type. + */ + static Map, String> getSupportedVectorTypes() { + + return Collections.unmodifiableMap(supportedVectorTypes); + } + + /** + * Generates the statement to create the index according to the vector field definition. + * + * @return the CREATE VECTOR INDEX statement to create the index according to the vector + * field definition. + */ + static String getCreateVectorIndexStatement(VectorStoreRecordVectorField field, String collectionTableName) { + switch (field.getIndexKind()) { + case IVFFLAT: + return "CREATE VECTOR INDEX IF NOT EXISTS " + + validateObjectNaming(getIndexName(field.getEffectiveStorageName())) + + " ON " + + validateObjectNaming(collectionTableName) + + "( " + validateObjectNaming(field.getEffectiveStorageName()) + " ) " + + " ORGANIZATION NEIGHBOR PARTITIONS " + + " WITH DISTANCE COSINE " + + "PARAMETERS ( TYPE IVF )"; + case HNSW: + return "CREATE VECTOR INDEX IF NOT EXISTS " + + validateObjectNaming(getIndexName(field.getEffectiveStorageName())) + + " ON " + + validateObjectNaming(collectionTableName) + + "( " + validateObjectNaming(field.getEffectiveStorageName()) + " ) " + + "ORGANIZATION INMEMORY GRAPH " + + "WITH DISTANCE COSINE " + + "PARAMETERS (TYPE HNSW)"; + case UNDEFINED: + return null; + default: + LOGGER.warning("Unsupported index kind: " + field.getIndexKind()); + return null; + } + } + + /** + * Generates the statement to create the index according to the field definition. + * + * @return the CREATE INDEX statement to create the index according to the field definition. + */ + static String createIndexForDataField(String collectionTableName, VectorStoreRecordDataField dataField, Map, String> supportedDataTypes) { + if (supportedDataTypes.get(dataField.getFieldType()) == "JSON") { + String dataFieldIndex = "CREATE MULTIVALUE INDEX IF NOT EXISTS %s ON %s t (t.%s.%s)"; + return String.format(dataFieldIndex, + validateObjectNaming(collectionTableName + "_" + dataField.getEffectiveStorageName()), + validateObjectNaming(collectionTableName), + validateObjectNaming(dataField.getEffectiveStorageName()), + getFunctionForType(supportedDataTypes.get(dataField.getFieldSubType()))); + } else { + String dataFieldIndex = "CREATE INDEX IF NOT EXISTS %s ON %s (%s ASC)"; + return String.format(dataFieldIndex, + validateObjectNaming(collectionTableName + "_" + dataField.getEffectiveStorageName()), + validateObjectNaming(collectionTableName), + validateObjectNaming(dataField.getEffectiveStorageName()) + ); + } + } + + /** + * Returns vector columns names and types for CREATE TABLE statement + * @param fields list of vector record fields. + * @return comma separated list of columns and types for CREATE TABLE statement. + */ + static String getVectorColumnNamesAndTypes(List fields) { + List columns = fields.stream() + .map(field -> validateObjectNaming(field.getEffectiveStorageName()) + " " + + OracleVectorStoreFieldHelper.getTypeForVectorField(field) + ).collect(Collectors.toList()); + + return String.join(", ", columns); + } + + /** + * Returns key column names and type for key column for CREATE TABLE statement + * @param field the key field. + * @return column name and type of the key field for CREATE TABLE statement. + */ + static String getKeyColumnNameAndType(VectorStoreRecordKeyField field) { + return validateObjectNaming(field.getEffectiveStorageName()) + " " + supportedKeyTypes.get(field.getFieldType()); + } + + + /** + * Generates the index name given the field name. by suffixing "_VECTOR_INDEX" to the field name. + * @param effectiveStorageName the field name. + * @return the index name. + */ + static String getIndexName(String effectiveStorageName) { + return effectiveStorageName + VECTOR_INDEX_SUFFIX; + } + + /** + * Gets the type of the vector given the field definition. This method is not needed if only + * + * @param field the vector field definition. + * @return returns the type of vector for the given field type. + */ + private static String getTypeForVectorField(VectorStoreRecordVectorField field) { + String dimension = field.getDimensions() > 0 ? String.valueOf(field.getDimensions()) : "*"; + return String.format(supportedVectorTypes.get(field.getFieldType()), dimension); + } + + /** + * Gets the function that allows to return the function that converts the JSON value to the + * data type. + * @param jdbcType The JDBC type. + * @return the function that allows to return the function that converts the JSON value to the + * data type. + */ + private static String getFunctionForType(String jdbcType) { + switch (jdbcType) { + case OracleDataTypesMapping.BOOLEAN: + return "boolean()"; + case OracleDataTypesMapping.BYTE: + case OracleDataTypesMapping.SHORT: + case OracleDataTypesMapping.INTEGER: + case OracleDataTypesMapping.LONG: + case OracleDataTypesMapping.FLOAT: + case OracleDataTypesMapping.DOUBLE: + case OracleDataTypesMapping.DECIMAL: + return "numberOnly()"; + case OracleDataTypesMapping.OFFSET_DATE_TIME: + return "timestamp()"; + default: + return "string()"; + } + } + + + /** + * Validates an SQL identifier. + * + * @param identifier the identifier + * @return the identifier if it is valid + * @throws SKException if the identifier is invalid + */ + static String validateObjectNaming(String identifier) { + if (identifier.matches(OBJECT_NAMING_REGEXP)) { + return identifier; + } + throw new SKException("Invalid SQL identifier: " + identifier); + } + +} diff --git a/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreQueryProvider.java b/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreQueryProvider.java new file mode 100644 index 000000000..e3fa157b9 --- /dev/null +++ b/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreQueryProvider.java @@ -0,0 +1,866 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.microsoft.semantickernel.data.filter.AnyTagEqualToFilterClause; +import com.microsoft.semantickernel.data.filter.EqualToFilterClause; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import javax.sql.DataSource; +import oracle.jdbc.OraclePreparedStatement; +import oracle.jdbc.OracleStatement; +import oracle.jdbc.OracleTypes; +import oracle.jdbc.provider.oson.OsonFactory; +import oracle.sql.TIMESTAMPTZ; + +/** + * JDBC Vector Store for the Oracle Database + */ +public class OracleVectorStoreQueryProvider extends JDBCVectorStoreQueryProvider { + + // This could be removed if super.collectionTable made protected + private final String collectionsTable; + + // This could be common to all query providers + private final ObjectMapper objectMapper; + + /** + * Lock used to ensure that only one thread can create a collection at a time. + */ + private static final ReentrantLock dbCreationLock = new ReentrantLock(); + + /** + * The logger + */ + private static final Logger LOGGER = Logger.getLogger(OracleVectorStoreQueryProvider.class.getName()); + + public enum StringTypeMapping { + /** + * Maps String to CLOB + */ + USE_CLOB, + /** + * Maps String to VARCHAR2(4000) + */ + USE_VARCHAR + } + + /** + * Create an instance of OracleVectorStoreQueryProvider. + * + * @param dataSource the datasource + * @param collectionsTable the collections table name + * @param prefixForCollectionTables the prefix for the collection table name + * @param defaultVarcharSize the size of VARCHAR columns + * @param stringTypeMapping the storage type of string columns (VARCHAR or CLOB) + * @param objectMapper the object mapper. + */ + private OracleVectorStoreQueryProvider( + @Nonnull DataSource dataSource, + @Nonnull String collectionsTable, + @Nonnull String prefixForCollectionTables, + int defaultVarcharSize, + @Nonnull StringTypeMapping stringTypeMapping, + ObjectMapper objectMapper) { + super( + dataSource, + collectionsTable, + prefixForCollectionTables, + OracleVectorStoreFieldHelper.getSupportedKeyTypes(), + OracleVectorStoreFieldHelper.getSupportedDataTypes(stringTypeMapping, defaultVarcharSize), + OracleVectorStoreFieldHelper.getSupportedVectorTypes()); + this.collectionsTable = collectionsTable; + this.objectMapper = objectMapper; + // The JavaTimeModule must be registered to handle OffsetDateTime. To make sure that it is + // registered enable the feature IGNORE_DUPLICATE_MODULE_REGISTRATIONS and register the + // module. + this.objectMapper.enable(MapperFeature.IGNORE_DUPLICATE_MODULE_REGISTRATIONS); + this.objectMapper.registerModule(new JavaTimeModule()); + } + + /** + *

+ * Creates a collection with the given name and record definition. + *

+ * A collection is represented as a table in an Oracle DB containing columns + * that match the record definition. The table name is the name of the collection + * prefixed by the provided collection prefix. If no prefix was provided the default + * prefix will be used. + *

+ * @param collectionName the name of the collection + * @param recordDefinition the record definition + */ + @Override + @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") + @GuardedBy("dbCreationLock") + public void createCollection(String collectionName, + VectorStoreRecordDefinition recordDefinition) { + + dbCreationLock.lock(); + try { + + List vectorFields = recordDefinition.getVectorFields(); + String createStorageTable = formatQuery("CREATE TABLE IF NOT EXISTS %s (" + + "%s PRIMARY KEY, " + + "%s, " + + "%s)", + getCollectionTableName(collectionName), + OracleVectorStoreFieldHelper.getKeyColumnNameAndType(recordDefinition.getKeyField()), + getColumnNamesAndTypes(new ArrayList<>(recordDefinition.getDataFields()), + getSupportedDataTypes()), + OracleVectorStoreFieldHelper.getVectorColumnNamesAndTypes( + new ArrayList<>(vectorFields))); + + String insertCollectionQuery = this.getInsertCollectionQuery(collectionsTable); + + try (Connection connection = dataSource.getConnection()) { + // set auto commit of, either all statements should be executed or none + connection.setAutoCommit(false); + try (Statement statement = connection.createStatement()) { + // Create table + statement.addBatch(createStorageTable); + LOGGER.finest("Creating collection " + collectionName + + " using statement: " + createStorageTable); + + // Index filterable data columns + for (VectorStoreRecordDataField dataField : recordDefinition.getDataFields()) { + if (dataField.isFilterable()) { + String dataFieldIndex = OracleVectorStoreFieldHelper.createIndexForDataField( + getCollectionTableName(collectionName), dataField, supportedDataTypes); + statement.addBatch(dataFieldIndex); + LOGGER.finest("Creating index on column " + + dataField.getEffectiveStorageName() + " using the statement: " + + dataFieldIndex); + } + } + + // Create index for vectorFields + for (VectorStoreRecordVectorField vectorField : vectorFields) { + String createVectorIndex = OracleVectorStoreFieldHelper.getCreateVectorIndexStatement( + vectorField, getCollectionTableName(collectionName)); + if (createVectorIndex != null) { + statement.addBatch(createVectorIndex); + LOGGER.finest("Creating index on vector column " + + vectorField.getEffectiveStorageName() + " using the statement: " + + createVectorIndex); + + } + } + statement.executeBatch(); + + // Insert the collection to the store (collections table) using MERGE statement + try (PreparedStatement insert = connection.prepareStatement( + insertCollectionQuery)) { + insert.setString(1, collectionName); + insert.execute(); + LOGGER.finest("Inserting collection to store using statement: " + + insertCollectionQuery); + } + + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw new SKException("Failed to create collection", e); + } + } catch (SQLException e) { + throw new SKException("Failed to create collection", e); + } + } finally { + dbCreationLock.unlock(); + } + } + + /** + *

+ * Inserts or updates record of a collection given the collection name, the records, the record + * definition and the upsert options. + *

+ * @Note At the moment {@link UpsertRecordOptions} is an empty class. No options are available. + *

+ * + * @param collectionName the collection name + * @param records the records to update or insert + * @param recordDefinition the record definition + * @param options the options + */ + @Override + @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") + public void upsertRecords(String collectionName, + List records, + VectorStoreRecordDefinition recordDefinition, + UpsertRecordOptions options) { + + final String NEW_VALUE = "new"; + final String EXISTING_VALUE = "existing"; + + // generate the comma separated list of new fields + // Ex.: new.field1, new.field2 ... new.fieldn + String insertNewFieldList = recordDefinition.getAllFields().stream() + .map(f -> NEW_VALUE + "." + f.getEffectiveStorageName()) + .collect(Collectors.joining(", ")); + + // generate the comma separated list of existing fields + // Ex.: existing.field1, existing.field2 ... existing.fieldn + String insertExistingFieldList = recordDefinition.getAllFields().stream() + .map(f -> EXISTING_VALUE + "." + f.getEffectiveStorageName()) + .collect(Collectors.joining(", ")); + + // generate the comma separated list for setting new values on fields + // Ex.: new.field1 = existing.field1, new.field2 = existing.field2 ... new.fieldn = existing.fieldn + String updateFieldList = recordDefinition.getAllFields().stream() + .filter(f -> f != recordDefinition.getKeyField()) + .map(f -> EXISTING_VALUE + "." + f.getEffectiveStorageName() + " = " + NEW_VALUE + "." + f.getEffectiveStorageName()) + .collect(Collectors.joining(", ")); + + // generate the comma separated list of placeholders "?" for each field + // Ex.: ? field1, ? field2 ... ? fieldn + String namedPlaceholders = recordDefinition.getAllFields().stream().map(f -> "? " + f.getEffectiveStorageName()) + .collect(Collectors.joining(", ")); + + // Generate the MERGE statement to perform the upsert. + String upsertStatement = formatQuery("MERGE INTO %s existing "+ + "USING (SELECT %s FROM DUAL) new ON (existing.%s = new.%s) " + + "WHEN MATCHED THEN UPDATE SET %s " + + "WHEN NOT MATCHED THEN INSERT (%s) VALUES (%s)", + getCollectionTableName(collectionName), + namedPlaceholders, + getKeyColumnName(recordDefinition.getKeyField()), + getKeyColumnName(recordDefinition.getKeyField()), + updateFieldList, + insertExistingFieldList, + insertNewFieldList); + + LOGGER.finest("Generated upsert statement: " + upsertStatement); + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(upsertStatement)) { + // Loop through records, set values and add values to batch + for (Object record : records) { + setUpsertStatementValues(statement, record, recordDefinition.getAllFields()); + statement.addBatch(); + } + + // Execute the upsert statement + statement.executeBatch(); + } catch (SQLException e) { + throw new SKException("Failed to upsert records", e); + } + } + + /** + * Generates the MERGE statement to add the given collection to the store. + * + * @param collectionsTable the name of the DB table containing all collections. + * @return a SQL statement that inserts a collection to the store if it does not exist. + */ + @Override + protected String getInsertCollectionQuery(String collectionsTable) { + return formatQuery( + "MERGE INTO %s existing "+ + "USING (SELECT ? AS collectionId FROM DUAL) new ON (existing.collectionId = new.collectionId) " + + "WHEN NOT MATCHED THEN INSERT (existing.collectionId) VALUES (new.collectionId)", + collectionsTable); + } + + /** + * The {@link OracleVectorStoreQueryProvider#upsertRecords(String, List, VectorStoreRecordDefinition, UpsertRecordOptions)} + * method adds a placeholder for each field. This method sets the value of each field on the + * MERGE statement with the value of the record. The placeholder and values are set in the order + * of the fields in the list. + * + * @param upsertStatement the MERGE statement + * @param record the record containing the values + * @param fields the list of fields. + */ + private void setUpsertStatementValues(PreparedStatement upsertStatement, Object record, + List fields) { + + // use the object mapper to convert the record to an equivalent tree mode JsonNode value, + // this allows to retrieve the values using the effective storage name of the fields and + // avoids the use of introspection. + JsonNode jsonNode = objectMapper.valueToTree(record); + + for (int i = 0; i < fields.size(); ++i) { + VectorStoreRecordField field = fields.get(i); + try { + + JsonNode valueNode = jsonNode.get(field.getEffectiveStorageName()); + + // Some field types require special treatment to convert the java type to the + // DB type + if (field instanceof VectorStoreRecordVectorField) { + // If the vector field is not set as a string convert to an array of floats + // and set the value + if (!field.getFieldType().equals(String.class)) { + if (valueNode != null && !valueNode.isNull() && valueNode.isArray()) { + final float[] values = new float[valueNode.size()]; + for (int j = 0; j < ((ArrayNode)valueNode).size(); j++) { + values[j] = ((ArrayNode)valueNode).get(j).floatValue(); + } + upsertStatement.setObject(i + 1, values, OracleTypes.VECTOR_FLOAT32); + } else { + upsertStatement.setNull(i + 1, OracleTypes.VECTOR_FLOAT32); + } + continue; + } + } else if (field instanceof VectorStoreRecordDataField) { + // Lists are stored as JSON objects, write the list using the JDBC OSON + // extensions. + if (field.getFieldType().equals(List.class)) { + JsonFactory osonFactory = new OsonFactory(); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + try (JsonGenerator osonGen = osonFactory.createGenerator(out)) { + objectMapper.writeValue(osonGen, valueNode); + } + upsertStatement.setBytes(i + 1, out.toByteArray()); + } catch (IOException ioEx) { + throw new SKException("Failed to convert list to JSON value", ioEx); + } + continue; + } + // Convert UUID to string before setting the value. + if (field.getFieldType().equals(UUID.class)) { + upsertStatement.setObject(i + 1, valueNode.isNull() ? null : valueNode.asText()); + continue; + } + // Convert value node (its representations depends on Jackson JSON features) + // to OffsetDateTime before setting the value. + if (field.getFieldType().equals(OffsetDateTime.class)) { + if (valueNode == null || valueNode.isNull()) { + upsertStatement.setNull(i + 1, OracleTypes.TIMESTAMPTZ); + } else { + OffsetDateTime offsetDateTime = (OffsetDateTime) objectMapper.convertValue(valueNode, field.getFieldType()); + upsertStatement.setObject(i + 1, offsetDateTime); + } + continue; + } + } + + // For all other field type use setObject with the field value + upsertStatement.setObject(i + 1, + objectMapper.convertValue(valueNode,field.getFieldType())); + } catch (SQLException e) { + throw new SKException(e); + } + } + } + + /** + *

+ * Executes a vector search query, using the search options and returns the results. The results + * are mapped to the specified record type using the provided mapper. The query is executed + * against the specified collection. + *

+ * + *

+ * @param collectionName the collection name + * @param vector the vector to search with + * @param options the search options + * @param recordDefinition the record definition + * @param mapper the mapper, responsible for mapping the result set to the record + * type. + * @return the search results + * @param the record type + */ + @Override + @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") + public VectorSearchResults search(String collectionName, List vector, + VectorSearchOptions options, VectorStoreRecordDefinition recordDefinition, + VectorStoreRecordMapper mapper) { + + + if (vector != null && recordDefinition.getVectorFields().isEmpty()) { + throw new SKException("Record definition must contain at least one vector field" + + " to perform a vector search"); + } + + // Gets the search vector field and its distance function. If not vector field was provided, + // use the first one + VectorStoreRecordVectorField vectorField = null; + if (vector != null) { + vectorField = getVectorFieldByName(recordDefinition, options.getVectorFieldName()); + } + + + + // get list of fields that should be returned by the query + List fields = (options.isIncludeVectors()) + ? recordDefinition.getAllFields() + : recordDefinition.getNonVectorFields(); + + // get search filters and get the list of parameters for the filters + String filter = getFilter(options.getVectorSearchFilter(), recordDefinition); + List parameters = getFilterParameters(options.getVectorSearchFilter()); + + // generate SQL statement + String selectQuery = "SELECT " + + (vector == null ? "0 as distance, " : + formatQuery("VECTOR_DISTANCE(%s, ?, %s) distance, ", + OracleVectorStoreFieldHelper.validateObjectNaming(vectorField.getEffectiveStorageName()), + toOracleDistanceFunction(vectorField.getDistanceFunction()))) + + getQueryColumnsFromFields(fields) + + " FROM " + getCollectionTableName(collectionName) + + (filter != null && !filter.isEmpty() ? " WHERE " + filter : "") + + " ORDER BY distance" + + (options.getSkip() > 0 ? " OFFSET " + options.getSkip() + " ROWS" : "") + + (options.getTop() > 0 ? " FETCH " + (options.getSkip() > 0 ? "NEXT " : "FIRST ") + options.getTop() + " ROWS ONLY" : ""); + LOGGER.finest("Search using statement: " + selectQuery); + + // Execute the statement + List> records = new ArrayList<>(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(selectQuery)) { + // set parameters from filters + int parameterIndex = 1; + // if a vector was provided for similarity search set the value of the vector + if (vector != null) { + float[] arrayVector = new float[vector.size()]; + for (int i = 0; i < vector.size(); i++){ + arrayVector[i] = vector.get(i).floatValue(); + } + statement.setObject(parameterIndex++, arrayVector, OracleTypes.VECTOR_FLOAT32); + } + // set all parameters. + for (Object parameter : parameters) { + if (parameter != null) { + setSearchParameter(statement, parameterIndex++, parameter.getClass(), parameter); + } + } + + // Calls to defineColumnType reduce the number of network requests. When Oracle JDBC knows that it is + // fetching VECTOR, CLOB, and/or JSON columns, the first request it sends to the database can include a LOB + // prefetch size (VECTOR and JSON are value-based-lobs). If defineColumnType is not called, then JDBC needs + // to send an additional request with the LOB prefetch size, after the first request has the database + // respond with the column data types. To request all data, the prefetch size is Integer.MAX_VALUE. + OracleStatement oracleStatement = statement.unwrap(OracleStatement.class); + int columnIndex = 1; + // define distance column as double + defineDataColumnType(columnIndex++, oracleStatement, Double.class); + // define columns for returned fields + for (VectorStoreRecordField field : fields) { + if (!(field instanceof VectorStoreRecordVectorField)) + defineDataColumnType(columnIndex++, oracleStatement, field.getFieldType()); + else + oracleStatement.defineColumnType(columnIndex++, OracleTypes.VECTOR_FLOAT32, + Integer.MAX_VALUE); + } + oracleStatement.setLobPrefetchSize(Integer.MAX_VALUE); // Workaround for Oracle JDBC bug 37030121 + + // Execute the statement and get the results + try (ResultSet rs = statement.executeQuery()) { + GetRecordOptions getRecordOptions = new GetRecordOptions(options.isIncludeVectors()); + while (rs.next()) { + // Cosine distance function. 1 - cosine similarity. + double score = Math.abs(rs.getDouble("distance")); + if (vector != null && vectorField.getDistanceFunction() == DistanceFunction.COSINE_SIMILARITY) { + score = 1d - score; + } + // Use the mapper to convert to result set to records + records.add(new VectorSearchResult<>(mapper.mapStorageModelToRecord(rs, getRecordOptions), score)); + } + } + } catch (SQLException e) { + throw new SKException("Search failed", e); + } + + return new VectorSearchResults<>(records); + } + + private VectorStoreRecordVectorField getVectorFieldByName( + VectorStoreRecordDefinition recordDefinition, + String name) { + VectorStoreRecordField vectorField; + if (name != null) { + vectorField = recordDefinition.getField(name); + if (vectorField == null) { + throw new SKException("Vector field not found in record definition"); + } + if (!(vectorField instanceof VectorStoreRecordVectorField)) { + throw new SKException("Invalid type"); + } + } else { + if (recordDefinition.getVectorFields().isEmpty()) { + throw new SKException("Record definition should contain at least one vector field"); + } + vectorField = recordDefinition.getVectorFields().get(0); + } + return (VectorStoreRecordVectorField)vectorField; + } + + /** + * Sets the parameter value + * @param statement the statement + * @param index the parameter index + * @param type the parameter type + * @param value the value + */ + private void setSearchParameter(PreparedStatement statement, int index, Class type, Object value) { + + try { + // Use JSON string to set lists + if (List.class.equals(type)) { + statement.setObject(index, objectMapper.writeValueAsString(value)); + return; + } + // convert UUID to string + if (UUID.class.equals(type)) { + statement.setString(index, value.toString()); + return; + } + // convert OffsetDateType to TIMESTAMPTZ + if (OffsetDateTime.class.equals(type)) { + if (value == null) { + statement.setNull(index, OracleTypes.TIMESTAMPTZ); + } else { + OffsetDateTime offsetDateTime = (OffsetDateTime) value; + ((OraclePreparedStatement) statement).setTIMESTAMPTZ(index, + TIMESTAMPTZ.of(offsetDateTime)); + } + return; + } + // use setBigDecimal to set BigDecimal value + if (BigDecimal.class.equals(type)) { + if (value == null) { + statement.setNull(index, OracleTypes.DECIMAL); + } else { + BigDecimal bigDecimal = (BigDecimal) value; + ((OraclePreparedStatement) statement).setBigDecimal(index, + bigDecimal); + } + return; + } + + // for all other types set object with the given value + statement.setObject(index, value); + + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + + /** + * Defines the type that will be used to retrieve data from a given database table column. + * @param columnIndex the index of the column + * @param statement the statement + * @param fieldType the java field type + * @throws SQLException if an error occurs while defining the column type + */ + private void defineDataColumnType(int columnIndex, OracleStatement statement, Class fieldType) throws SQLException { + // switch between supported classes and define the column type on the statement + switch (supportedDataTypes.get(fieldType)) { + case OracleDataTypesMapping.STRING_CLOB: + statement.defineColumnType(columnIndex, OracleTypes.CLOB, Integer.MAX_VALUE); + break; + case OracleDataTypesMapping.BYTE: + statement.defineColumnType(columnIndex, OracleTypes.NUMBER); + break; + case OracleDataTypesMapping.SHORT: + statement.defineColumnType(columnIndex, OracleTypes.NUMBER); + break; + case OracleDataTypesMapping.INTEGER: + statement.defineColumnType(columnIndex, OracleTypes.INTEGER); + break; + case OracleDataTypesMapping.LONG: + statement.defineColumnType(columnIndex, OracleTypes.BIGINT); + break; + case OracleDataTypesMapping.FLOAT: + statement.defineColumnType(columnIndex, OracleTypes.BINARY_FLOAT); + break; + case OracleDataTypesMapping.DOUBLE: + statement.defineColumnType(columnIndex, OracleTypes.BINARY_DOUBLE); + break; + case OracleDataTypesMapping.DECIMAL: + statement.defineColumnType(columnIndex, OracleTypes.BINARY_DOUBLE); + break; + case OracleDataTypesMapping.BOOLEAN: + statement.defineColumnType(columnIndex, OracleTypes.BOOLEAN); + break; + case OracleDataTypesMapping.OFFSET_DATE_TIME: + statement.defineColumnType(columnIndex, OracleTypes.TIMESTAMPTZ); + break; + case OracleDataTypesMapping.JSON: + statement.defineColumnType(columnIndex, OracleTypes.JSON, Integer.MAX_VALUE); + break; + case OracleDataTypesMapping.BYTE_ARRAY: + statement.defineColumnType(columnIndex, OracleTypes.RAW); + default: + statement.defineColumnType(columnIndex, OracleTypes.VARCHAR); + } + } + + /** + * Converts a {@link DistanceFunction} to the equivalent Oracle distance function. + * @param distanceFunction the distance function + * @return the Oracle distance function + */ + private String toOracleDistanceFunction(DistanceFunction distanceFunction) { + switch (distanceFunction) { + case DOT_PRODUCT: + return "DOT"; + case COSINE_SIMILARITY: + case COSINE_DISTANCE: + return "COSINE"; + case EUCLIDEAN_DISTANCE: + return "EUCLIDEAN"; + default: + return "COSINE"; + } + } + + /** + * Gets the filter parameters for the given vector search filter to associate with the filter + * string generated by the getFilter method. + * + * @param filter The filter to get the filter parameters for. + * @return The filter parameters. + */ + @Override + public List getFilterParameters(VectorSearchFilter filter) { + // TODO: this method should be protected, not public + if (filter == null + || filter.getFilterClauses().isEmpty()) { + return Collections.emptyList(); + } + + return filter.getFilterClauses().stream().map(filterClause -> { + if (filterClause instanceof EqualToFilterClause) { + EqualToFilterClause equalToFilterClause = (EqualToFilterClause) filterClause; + return equalToFilterClause.getValue(); + } else if (filterClause instanceof AnyTagEqualToFilterClause) { + AnyTagEqualToFilterClause anyTagEqualToFilterClause = (AnyTagEqualToFilterClause) filterClause; + return anyTagEqualToFilterClause.getValue(); + } else { + throw new SKException("Unsupported filter clause type '" + + filterClause.getClass().getSimpleName() + "'."); + } + }).collect(Collectors.toList()); + } + + /** + * Gets the filter clause for an equal to filter + * @param filterClause The equal to filter clause to get the filter string for. + * @return the filter clause + */ + @Override + public String getEqualToFilter(EqualToFilterClause filterClause) { + String fieldName = JDBCVectorStoreQueryProvider + .validateSQLidentifier(filterClause.getFieldName()); + Object value = filterClause.getValue(); + + if (value == null) { + return String.format("%s is NULL", fieldName); + } else { + return String.format("%s = ?", fieldName); + } + } + + /** + * Gets the filter clause for an any tag equal to filter + * @param filterClause The any tag equal to filter clause to get the filter string for. + * @return the filter clause + */ + @Override + public String getAnyTagEqualToFilter(AnyTagEqualToFilterClause filterClause) { + String fieldName = JDBCVectorStoreQueryProvider + .validateSQLidentifier(filterClause.getFieldName()); + + return String.format("JSON_EXISTS(%s, '$[*]?(@ == $v_%s)' PASSING ? AS \"v_%s\")", + fieldName, fieldName, fieldName); + } + + /** + * Gets the mapper used to map a ResultSet to records + * @param recordClass the record class + * @param vectorStoreRecordDefinition the record definition + * @return the vector store record mapper + * @param the type of the records + */ + @Override + public VectorStoreRecordMapper getVectorStoreRecordMapper( + Class recordClass, + VectorStoreRecordDefinition vectorStoreRecordDefinition) { + return OracleVectorStoreRecordMapper.builder() + .withRecordClass(recordClass) + .withVectorStoreRecordDefinition(vectorStoreRecordDefinition) + .withSupportedDataTypesMapping(getSupportedDataTypes()) + .build(); + } + + /** + * Gets a builder that allows to build an OracleVectorStoreQueryProvider + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * OracleVectorStoreQueryProvider builder. + */ + public static class Builder + extends JDBCVectorStoreQueryProvider.Builder { + + /** + * The data source + */ + private DataSource dataSource; + + /** + * The collections table + */ + private String collectionsTable = DEFAULT_COLLECTIONS_TABLE; + + /** + * The prefix for collection table names + */ + private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; + + /** + * The object mapper + */ + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * The string type mapping choice + */ + private StringTypeMapping stringTypeMapping = StringTypeMapping.USE_VARCHAR; + + /** + * The size of varchar columns + */ + private int defaultVarcharSize = 2000; + + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withDataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + /** + * Sets the collections table name. + * @param collectionsTable the collections table name + * @return the builder + */ + public Builder withCollectionsTable(String collectionsTable) { + this.collectionsTable = validateSQLidentifier(collectionsTable); + return this; + } + + /** + * Sets the prefix for collection tables. + * @param prefixForCollectionTables the prefix for collection tables + * @return the builder + */ + public Builder withPrefixForCollectionTables(String prefixForCollectionTables) { + this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); + return this; + } + + /** + * Sets the object mapper used to map records to and from results + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper( + ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Sets the desired String type mapping. + * @param stringTypeMapping the desired String type mapping. The default value is + * {@link StringTypeMapping#USE_VARCHAR} + * @return the builder + */ + public Builder withStringTypeMapping (StringTypeMapping stringTypeMapping) { + this.stringTypeMapping = stringTypeMapping; + return this; + } + + /** + * Sets the default size of the VARHCHAR fields. + * @param defaultVarcharSize the default size of the VARHCHAR fields. By default, the size + * is 2000. + * @return then builder + */ + public Builder withDefaultVarcharSize (int defaultVarcharSize) { + this.defaultVarcharSize = defaultVarcharSize; + return this; + } + + /** + * Builds and Oracle vector store query provider. + * @return the query provider + */ + @Override + public OracleVectorStoreQueryProvider build() { + return new OracleVectorStoreQueryProvider(dataSource, collectionsTable, + prefixForCollectionTables, defaultVarcharSize, stringTypeMapping, objectMapper); + } + } +} + diff --git a/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreRecordMapper.java b/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreRecordMapper.java new file mode 100644 index 000000000..c75eb6631 --- /dev/null +++ b/data/semantickernel-data-oracle/src/main/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreRecordMapper.java @@ -0,0 +1,264 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import oracle.jdbc.provider.oson.OsonModule; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; +import java.util.HashMap; +import java.util.UUID; +import java.util.function.BiFunction; + +/** + * Maps a Oracle result set to a record. + * + * @param the record type + */ +public class OracleVectorStoreRecordMapper + extends VectorStoreRecordMapper { + + /** + * Constructs a new instance of the VectorStoreRecordMapper. + * + * @param storageModelToRecordMapper the function to convert a storage model to a record + */ + protected OracleVectorStoreRecordMapper( + BiFunction storageModelToRecordMapper) { + super(null, storageModelToRecordMapper); + } + + /** + * Creates a new builder. + * + * @param the record type + * @return the builder + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * Operation not supported. + */ + @Override + public ResultSet mapRecordToStorageModel(Record record) { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Builder for {@link OracleVectorStoreRecordMapper}. + * + * @param the record type + */ + public static class Builder + implements SemanticKernelBuilder> { + private Class recordClass; + private VectorStoreRecordDefinition vectorStoreRecordDefinition; + private Map, String> supportedDataTypesMapping; + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Sets the record class. + * + * @param recordClass the record class + * @return the builder + */ + public Builder withRecordClass(Class recordClass) { + this.recordClass = recordClass; + return this; + } + + /** + * Sets the vector store record definition. + * + * @param vectorStoreRecordDefinition the vector store record definition + * @return the builder + */ + public Builder withVectorStoreRecordDefinition( + VectorStoreRecordDefinition vectorStoreRecordDefinition) { + this.vectorStoreRecordDefinition = vectorStoreRecordDefinition; + return this; + } + + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Sets the Map of supported data types and their database representation + * + * @param supportedDataTypesMapping the Map of supported data types and their + * database representation + * @return the builder + */ + public Builder withSupportedDataTypesMapping( + Map, String> supportedDataTypesMapping) { + this.supportedDataTypesMapping = new HashMap<>(supportedDataTypesMapping); + return this; + } + + /** + * Builds the {@link OracleVectorStoreRecordMapper}. + * + * @return the {@link OracleVectorStoreRecordMapper} + */ + public OracleVectorStoreRecordMapper build() { + if (recordClass == null) { + throw new SKException("recordClass is required"); + } + if (vectorStoreRecordDefinition == null) { + throw new SKException("vectorStoreRecordDefinition is required"); + } + + return new OracleVectorStoreRecordMapper<>( + (resultSet, options) -> { + return mapResultSetToRecord(resultSet, options); + }); + } + + private Record mapResultSetToRecord(ResultSet resultSet, GetRecordOptions options) { + try { + objectMapper.registerModule(new OsonModule()); + // Create an ObjectNode to hold the values + ObjectNode objectNode = objectMapper.createObjectNode(); + + // Read non vector fields + for (VectorStoreRecordField field : vectorStoreRecordDefinition.getNonVectorFields()) { + Class fieldType = field.getFieldType(); + + Object value; + switch (supportedDataTypesMapping.get(fieldType)) { + case OracleDataTypesMapping.STRING_CLOB: + value = resultSet.getString(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.BYTE: + value = resultSet.getByte(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.SHORT: + value = resultSet.getShort(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.INTEGER: + value = resultSet.getInt(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.LONG: + value = resultSet.getLong(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.FLOAT: + value = resultSet.getFloat(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.DOUBLE: + value = resultSet.getDouble(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.DECIMAL: + value = resultSet.getBigDecimal(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.BOOLEAN: + value = resultSet.getBoolean(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.OFFSET_DATE_TIME: + value = resultSet.getObject(field.getEffectiveStorageName(), fieldType); + break; + case OracleDataTypesMapping.BYTE_ARRAY: + value = resultSet.getBytes(field.getEffectiveStorageName()); + break; + case OracleDataTypesMapping.UUID: + String uuidValue = resultSet.getString(field.getEffectiveStorageName()); + value = uuidValue == null ? null : UUID.fromString(uuidValue); + break; + case OracleDataTypesMapping.JSON: + value = resultSet.getObject(field.getEffectiveStorageName(), fieldType); + break; + default: + value = resultSet.getString(field.getEffectiveStorageName()); + } + // Result set getter method sometimes returns a default value when NULL, + // set value to null in that case. + if (resultSet.wasNull()) { + value = null; + } + + JsonNode genericNode = objectMapper.valueToTree(value); + + objectNode.set(field.getEffectiveStorageName(), genericNode); + } + if (options != null && options.isIncludeVectors()) { + for (VectorStoreRecordVectorField field : vectorStoreRecordDefinition.getVectorFields()) { + + // String vector + if (field.getFieldType().equals(String.class)) { + float[] arr = resultSet.getObject(field.getEffectiveStorageName(), float[].class); + String str = (arr == null) + ? null + : objectMapper.writeValueAsString(arr); + objectNode.put(field.getEffectiveStorageName(), str); + continue; + } + + Object value = resultSet.getObject(field.getEffectiveStorageName(), float[].class); + JsonNode genericNode = objectMapper.valueToTree(value); + objectNode.set(field.getEffectiveStorageName(), genericNode); + } + } else { + for (VectorStoreRecordVectorField field : vectorStoreRecordDefinition.getVectorFields()) { + JsonNode genericNode = objectMapper.valueToTree(null); + objectNode.set(field.getEffectiveStorageName(), genericNode); + } + } + + // Deserialize the object node to the record class + return objectMapper.convertValue(objectNode, recordClass); + } catch (SQLException e) { + throw new SKException( + "Failure to serialize object, by default the JDBC connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", + e); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAllBoxedTypes.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAllBoxedTypes.java new file mode 100644 index 000000000..14b96c142 --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAllBoxedTypes.java @@ -0,0 +1,162 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public class ClassWithAllBoxedTypes { + + @VectorStoreRecordKey + private final String id; + + @VectorStoreRecordData(isFilterable = true) + private final Boolean booleanValue; + + @VectorStoreRecordData(isFilterable = true) + private final Byte byteValue; + + @VectorStoreRecordData(isFilterable = true) + private final Short shortValue; + + @VectorStoreRecordData(isFilterable = true) + private final Integer integerValue; + + @VectorStoreRecordData(isFilterable = true) + private final Long longValue; + + @VectorStoreRecordData(isFilterable = true) + private final Float floatValue; + + @VectorStoreRecordData(isFilterable = true) + private final Double doubleValue; + + @VectorStoreRecordData(isFilterable = true) + private final BigDecimal decimalValue; + + @VectorStoreRecordData(isFilterable = true) + private final OffsetDateTime offsetDateTimeValue; + + @VectorStoreRecordData(isFilterable = true) + private final UUID uuidValue; + + @VectorStoreRecordData(isFilterable = true) + private final byte[] byteArrayValue; + + @VectorStoreRecordData(isFilterable = true) + private final List listOfFloatValue; + + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE, indexKind = IndexKind.IVFFLAT) + private final Float[] vectorValue; + + + public ClassWithAllBoxedTypes() { + this(null, false, Byte.MIN_VALUE,Short.MIN_VALUE, 0, 0l, 0f, 0d, null, null, null, null, null, null); + }; + public ClassWithAllBoxedTypes(String id, Boolean booleanValue, Byte byteValue, + Short shortValue, Integer integerValue, Long longValue, Float floatValue, Double doubleValue, + BigDecimal decimalValue, OffsetDateTime offsetDateTimeValue, UUID uuidValue, + byte[] byteArrayValue, List listOfFloatValue, Float[] vectorValue) { + this.id = id; + this.booleanValue = booleanValue; + this.byteValue = byteValue; + this.shortValue = shortValue; + this.integerValue = integerValue; + this.longValue = longValue; + this.floatValue = floatValue; + this.doubleValue = doubleValue; + this.decimalValue = decimalValue; + this.offsetDateTimeValue = offsetDateTimeValue; + this.uuidValue = uuidValue; + this.byteArrayValue = byteArrayValue; + this.listOfFloatValue = listOfFloatValue; + this.vectorValue = vectorValue; + } + + public String getId() { + return id; + } + + public Boolean getBooleanValue() { + return booleanValue; + } + + public Byte getByteValue() { + return byteValue; + } + + public Short getShortValue() { + return shortValue; + } + + public Integer getIntegerValue() { + return integerValue; + } + + public Long getLongValue() { + return longValue; + } + + public Float getFloatValue() { + return floatValue; + } + + public Double getDoubleValue() { + return doubleValue; + } + + + public BigDecimal getDecimalValue() { + return decimalValue; + } + + public OffsetDateTime getOffsetDateTimeValue() { + return offsetDateTimeValue; + } + + public UUID getUuidValue() { + return uuidValue; + } + + public byte[] getByteArrayValue() { + return byteArrayValue; + } + + public List getListOfFloatValue() { + return listOfFloatValue; + } + + public Float[] getVectorValue() { + return vectorValue; + } +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAllPrimitiveTypes.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAllPrimitiveTypes.java new file mode 100644 index 000000000..6a8d76d07 --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAllPrimitiveTypes.java @@ -0,0 +1,163 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public class ClassWithAllPrimitiveTypes { + + @VectorStoreRecordKey + private final String id; + + @VectorStoreRecordData(isFilterable = true) + private final Boolean booleanValue; + + @VectorStoreRecordData(isFilterable = true) + private final byte byteValue; + + @VectorStoreRecordData(isFilterable = true) + private final short shortValue; + + @VectorStoreRecordData(isFilterable = true) + private final int integerValue; + + @VectorStoreRecordData(isFilterable = true) + private final long longValue; + + @VectorStoreRecordData(isFilterable = true) + private final float floatValue; + + @VectorStoreRecordData(isFilterable = true) + private final double doubleValue; + + @VectorStoreRecordData(isFilterable = true) + private final BigDecimal decimalValue; + + @VectorStoreRecordData(isFilterable = true) + private final OffsetDateTime offsetDateTimeValue; + + @VectorStoreRecordData(isFilterable = true) + private final UUID uuidValue; + + @VectorStoreRecordData(isFilterable = true) + private final byte[] byteArrayValue; + + @VectorStoreRecordData(isFilterable = true) + private final List listOfFloatValue; + + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE, indexKind = IndexKind.IVFFLAT) + private final float[] vectorValue; + + + public ClassWithAllPrimitiveTypes() { + this(null, false, Byte.MIN_VALUE,Short.MIN_VALUE, 0, 0l, 0f, 0d, null, null, null, null, null, null); + }; + public ClassWithAllPrimitiveTypes(String id, boolean booleanValue, byte byteValue, + short shortValue, int integerValue, long longValue, float floatValue, double doubleValue, + BigDecimal decimalValue, OffsetDateTime offsetDateTimeValue, UUID uuidValue, + byte[] byteArrayValue, List listOfFloatValue, float[] vectorValue) { + this.id = id; + this.booleanValue = booleanValue; + this.byteValue = byteValue; + this.shortValue = shortValue; + this.integerValue = integerValue; + this.longValue = longValue; + this.floatValue = floatValue; + this.doubleValue = doubleValue; + this.decimalValue = decimalValue; + this.offsetDateTimeValue = offsetDateTimeValue; + this.uuidValue = uuidValue; + this.byteArrayValue = byteArrayValue; + this.listOfFloatValue = listOfFloatValue; + this.vectorValue = vectorValue; + } + + public String getId() { + return id; + } + + public boolean getBooleanValue() { + return booleanValue; + } + + public byte getByteValue() { + return byteValue; + } + + public short getShortValue() { + return shortValue; + } + + public int getIntegerValue() { + return integerValue; + } + + public long getLongValue() { + return longValue; + } + + public float getFloatValue() { + return floatValue; + } + + public double getDoubleValue() { + return doubleValue; + } + + + public BigDecimal getDecimalValue() { + return decimalValue; + } + + public OffsetDateTime getOffsetDateTimeValue() { + return offsetDateTimeValue; + } + + public UUID getUuidValue() { + return uuidValue; + } + + public byte[] getByteArrayValue() { + return byteArrayValue; + } + + public List getListOfFloatValue() { + return listOfFloatValue; + } + + public float[] getVectorValue() { + return vectorValue; + } +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAnnotatedTypes.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAnnotatedTypes.java new file mode 100644 index 000000000..75190debf --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/ClassWithAnnotatedTypes.java @@ -0,0 +1,91 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public class ClassWithAnnotatedTypes { + + private final String id; + + @JsonProperty("value_type") + private final String valueType; + + @JsonProperty("value_field") + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.EXTERNAL_PROPERTY , property = "value_type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = String.class, name="string"), + @JsonSubTypes.Type(value = Boolean.class, name="boolean"), + @JsonSubTypes.Type(value = Byte.class, name="byte"), + @JsonSubTypes.Type(value = Short.class, name="short"), + @JsonSubTypes.Type(value = Integer.class, name="integer"), + @JsonSubTypes.Type(value = Long.class, name="long"), + @JsonSubTypes.Type(value = Float.class, name="float"), + @JsonSubTypes.Type(value = Double.class, name="double"), + @JsonSubTypes.Type(value = BigDecimal.class, name="decimal"), + @JsonSubTypes.Type(value = OffsetDateTime.class, name="timestamp"), + @JsonSubTypes.Type(value = UUID.class, name="uuid"), + @JsonSubTypes.Type(value = byte[].class, name="byte_array"), + @JsonSubTypes.Type(value = List.class, name="json") + }) + private Object value; + + private final Float[] vectorValue; + + + public ClassWithAnnotatedTypes() { + this(null, null, null, null); + }; + public ClassWithAnnotatedTypes(String id, String valueType, Object value, Float[] vectorValue) { + this.id = id; + this.valueType = valueType; + this.value = value; + this.vectorValue = vectorValue; + } + + public String getId() { + return id; + } + + public String getValueType() { return valueType; } + + public Object getValue() { + return value; + } + + public Float[] getVectorValue() { + return vectorValue; + } +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/Hotel.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/Hotel.java new file mode 100644 index 000000000..ef6ac824f --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/Hotel.java @@ -0,0 +1,116 @@ + +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import java.util.List; + +public class Hotel { + @VectorStoreRecordKey + private final String id; + + @VectorStoreRecordData(isFilterable = true) + private final String name; + + @VectorStoreRecordData + private final int code; + + @VectorStoreRecordData + private final double price; + + @VectorStoreRecordData(isFilterable = true) + private final List tags; + + @JsonProperty("summary") + @VectorStoreRecordData( isFilterable = true, isFullTextSearchable = true ) + private final String description; + + @JsonProperty("summaryEmbedding1") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.EUCLIDEAN_DISTANCE, indexKind = IndexKind.IVFFLAT) + private final List euclidean; + + @JsonProperty("summaryEmbedding2") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE, indexKind = IndexKind.HNSW) + private final float[] cosineDistance; + + @JsonProperty("summaryEmbedding3") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_SIMILARITY, indexKind = IndexKind.IVFFLAT) + private final float[] cosineSimilarity; + + @JsonProperty("summaryEmbedding4") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.DOT_PRODUCT, indexKind = IndexKind.IVFFLAT) + private final Float[] dotProduct; + @VectorStoreRecordData + private double rating; + + @JsonCreator + public Hotel( + @JsonProperty("id") String id, + @JsonProperty("name") String name, + @JsonProperty("code") int code, + @JsonProperty("price") double price, + @JsonProperty("tags") List tags, + @JsonProperty("summary") String description, + @JsonProperty("summaryEmbedding1") List euclidean, + @JsonProperty("summaryEmbedding2") float[] cosineDistance, + @JsonProperty("summaryEmbedding3") float[] cosineSimilarity, + @JsonProperty("summaryEmbedding4") Float[] dotProduct, + @JsonProperty("rating") double rating) { + this.id = id; + this.name = name; + this.code = code; + this.price = price; + this.tags = tags; + this.description = description; + this.euclidean = euclidean; + this.cosineDistance = cosineDistance; + this.cosineSimilarity = cosineSimilarity; + this.dotProduct = dotProduct; + this.rating = rating; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getCode() { + return code; + } + + public double getPrice() { return price; } + + public List getTags() { return tags; } + + public String getDescription() { + return description; + } + + public List getEuclidean() { + return euclidean; + } + + public float[] getCosineDistance() { + return cosineDistance; + } + + public Float[] getDotProduct() { + return dotProduct; + } + + public double getRating() { + return rating; + } + + public void setRating(double rating) { + this.rating = rating; + } +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleCommonVectorStoreRecordCollectionTest.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleCommonVectorStoreRecordCollectionTest.java new file mode 100644 index 000000000..ce260c77e --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleCommonVectorStoreRecordCollectionTest.java @@ -0,0 +1,91 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import oracle.jdbc.OracleConnection; +import oracle.jdbc.datasource.impl.OracleDataSource; +import org.testcontainers.oracle.OracleContainer; +import org.testcontainers.utility.MountableFile; +import java.sql.SQLException; +import java.time.Duration; + +public class OracleCommonVectorStoreRecordCollectionTest { + + protected static final String ORACLE_IMAGE_NAME = "gvenzl/oracle-free:23.7-slim-faststart"; + protected static final OracleDataSource DATA_SOURCE; + protected static final OracleDataSource SYSDBA_DATA_SOURCE; + + static { + + try { + DATA_SOURCE = new oracle.jdbc.datasource.impl.OracleDataSource(); + SYSDBA_DATA_SOURCE = new oracle.jdbc.datasource.impl.OracleDataSource(); + String urlFromEnv = System.getenv("ORACLE_JDBC_URL"); + + if (urlFromEnv == null) { + // The Ryuk component is relied upon to stop this container. + OracleContainer oracleContainer = new OracleContainer(ORACLE_IMAGE_NAME) + .withCopyFileToContainer(MountableFile.forClasspathResource("/initialize.sql"), + "/container-entrypoint-initdb.d/initialize.sql") + .withStartupTimeout(Duration.ofSeconds(600)) + .withConnectTimeoutSeconds(600) + .withDatabaseName("pdb1") + .withUsername("testuser") + .withPassword("testpwd"); + oracleContainer.start(); + + initDataSource( + DATA_SOURCE, + oracleContainer.getJdbcUrl(), + oracleContainer.getUsername(), + oracleContainer.getPassword()); + initDataSource(SYSDBA_DATA_SOURCE, oracleContainer.getJdbcUrl(), "sys", oracleContainer.getPassword()); + } else { + initDataSource( + DATA_SOURCE, + urlFromEnv, + System.getenv("ORACLE_JDBC_USER"), + System.getenv("ORACLE_JDBC_PASSWORD")); + initDataSource( + SYSDBA_DATA_SOURCE, + urlFromEnv, + System.getenv("ORACLE_JDBC_USER"), + System.getenv("ORACLE_JDBC_PASSWORD")); + } + SYSDBA_DATA_SOURCE.setConnectionProperty(OracleConnection.CONNECTION_PROPERTY_INTERNAL_LOGON, "SYSDBA"); + + } catch (SQLException sqlException) { + throw new AssertionError(sqlException); + } + } + + static void initDataSource(OracleDataSource dataSource, String url, String username, String password) { + dataSource.setURL(url); + dataSource.setUser(username); + dataSource.setPassword(password); + } + +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreAnnotatedTypeTest.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreAnnotatedTypeTest.java new file mode 100644 index 000000000..e0332950b --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreAnnotatedTypeTest.java @@ -0,0 +1,163 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class OracleVectorStoreAnnotatedTypeTest extends OracleCommonVectorStoreRecordCollectionTest { + + @ParameterizedTest + @MethodSource("supportedDataTypes") + void testDataTypes(String dataFieldName, Class dataFieldType, Object dataFieldValue, Class fieldSubType) { + VectorStoreRecordKeyField keyField = VectorStoreRecordKeyField.builder() + .withName("id") + .withStorageName("id") + .withFieldType(String.class) + .build(); + + VectorStoreRecordDataField dataField; + if (fieldSubType != null) { + dataField = VectorStoreRecordDataField.builder() + .withName("value") + .withStorageName("value_field") + .withFieldType(dataFieldType, fieldSubType) + .isFilterable(true) + .build(); + } else { + dataField = VectorStoreRecordDataField.builder() + .withName("value") + .withStorageName("value_field") + .withFieldType(dataFieldType) + .isFilterable(true) + .build(); + } + VectorStoreRecordDataField dataTypeField; + dataTypeField = VectorStoreRecordDataField.builder() + .withName("valueType") + .withStorageName("value_type") + .withFieldType(String.class) + .isFilterable(false) + .build(); + + + VectorStoreRecordVectorField dummyVector = VectorStoreRecordVectorField.builder() + .withName("vectorValue") + .withStorageName("vectorValue") + .withFieldType(Float[].class) + .withDimensions(8) + .withDistanceFunction(DistanceFunction.COSINE_DISTANCE) + .withIndexKind(IndexKind.IVFFLAT) + .build(); + + VectorStoreRecordDefinition definition = VectorStoreRecordDefinition.fromFields( + Arrays.asList(keyField, dataTypeField, dataField, dummyVector) + ); + + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE) + .build(); + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + String collectionName = "test_datatype_" + dataFieldName; + + VectorStoreRecordCollection collection = + vectorStore.getCollection(collectionName, + JDBCVectorStoreRecordCollectionOptions. builder() + .withRecordClass(ClassWithAnnotatedTypes.class) + .withRecordDefinition(definition).build()); + + collection.createCollectionAsync().block(); + + String key = "testid"; + + ClassWithAnnotatedTypes record = + new ClassWithAnnotatedTypes(key, dataFieldName, dataFieldValue, new Float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f }); + + collection.upsertAsync(record, null).block(); + + ClassWithAnnotatedTypes result = collection.getAsync(key, null).block(); + assertNotNull(result); + if (record.getValue().getClass().equals(OffsetDateTime.class)) { + assertTrue(((OffsetDateTime)dataFieldValue).isEqual((OffsetDateTime)record.getValue())); + } else if (dataFieldName == "byte_array") { + assertArrayEquals((byte[]) dataFieldValue, (byte[])record.getValue()); + } else { + assertEquals(dataFieldValue, result.getValue()); + } + + collection.deleteCollectionAsync().block(); + } + + private static Stream supportedDataTypes() { + return Stream.of( + Arguments.of("string", String.class, "asd123", null), + Arguments.of("boolean", Boolean.class, true, null), + Arguments.of("boolean", Boolean.class, false, null), + Arguments.of("byte", Byte.class, (byte) 127, null), + Arguments.of("short", Short.class, (short) 3, null), + Arguments.of("integer", Integer.class, 321, null), + Arguments.of("long", Long.class, 5L, null), + Arguments.of("float", Float.class, 3.14f, null), + Arguments.of("double", Double.class, 3.14159265358d, null), + Arguments.of("decimal", BigDecimal.class, new BigDecimal("12345.67"), null), + Arguments.of("timestamp", OffsetDateTime.class, OffsetDateTime.now(), null), + Arguments.of("uuid", UUID.class, UUID.randomUUID(), null), + Arguments.of("byte_array", byte[].class, new byte[] {1, 2, 3}, String.class), + Arguments.of("json", List.class, Arrays.asList("a", "s", "d"), String.class) + ); + } + +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreDataTypeSearchTest.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreDataTypeSearchTest.java new file mode 100644 index 000000000..bac0ee4c3 --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreDataTypeSearchTest.java @@ -0,0 +1,321 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OracleVectorStoreDataTypeSearchTest extends OracleCommonVectorStoreRecordCollectionTest { + private static final double MIN_DOUBLE = 1.0E-130; + private static final double MIN_DECIMAL = -1.0E125; + private static final BigDecimal BIG_NUMBER = BigDecimal.valueOf(9999999999999999.99); + + + + @ParameterizedTest + @MethodSource("supportedDataTypes") + void testDataTypesSearch (ClassWithAllBoxedTypes record) { + VectorStoreRecordCollection collection = setupBoxed(); + + collection.upsertAsync(record, null).block(); + + // boolean + VectorSearchResults results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("booleanValue", record.getBooleanValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals(record.getBooleanValue(), results.getResults().get(0).getRecord().getBooleanValue()); + + // byte + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("byteValue", record.getByteValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals(record.getByteValue(), results.getResults().get(0).getRecord().getByteValue()); + + // short + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("shortValue", record.getShortValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals(record.getShortValue(), results.getResults().get(0).getRecord().getShortValue()); + + // integer + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("integerValue", record.getIntegerValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals(record.getIntegerValue(), results.getResults().get(0).getRecord().getIntegerValue()); + + // long + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("longValue", record.getLongValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals(record.getLongValue(), results.getResults().get(0).getRecord().getLongValue()); + + // float + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("floatValue", record.getFloatValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals(record.getFloatValue(), results.getResults().get(0).getRecord().getFloatValue()); + + // double + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("doubleValue", record.getDoubleValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals(record.getDoubleValue(), results.getResults().get(0).getRecord().getDoubleValue()); + + // decimal + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("decimalValue", record.getDecimalValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + if (record.getDecimalValue() != null) { + assertEquals(0, record.getDecimalValue() + .compareTo(results.getResults().get(0).getRecord().getDecimalValue())); + } else { + assertEquals(record.getDecimalValue(), + results.getResults().get(0).getRecord().getDecimalValue()); + } + + // offset date time + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("offsetDateTimeValue", record.getOffsetDateTimeValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + if (record.getOffsetDateTimeValue() != null) { + assertTrue(record.getOffsetDateTimeValue() + .isEqual(results.getResults().get(0).getRecord().getOffsetDateTimeValue())); + } else { + assertEquals(record.getOffsetDateTimeValue(), + results.getResults().get(0).getRecord().getOffsetDateTimeValue()); + } + + // UUID + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("uuidValue", record.getUuidValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals(record.getUuidValue(), results.getResults().get(0).getRecord().getUuidValue()); + + // byte array + results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("byteArrayValue", record.getByteArrayValue()).build() + ).build()).block(); + + assertEquals(1, results.getTotalCount()); + assertArrayEquals(record.getByteArrayValue(), results.getResults().get(0).getRecord().getByteArrayValue()); + + collection.deleteCollectionAsync().block(); + + } + + + public VectorStoreRecordCollection setupBoxed() { + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE) + .build(); + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + VectorStoreRecordCollection collection = + vectorStore.getCollection("BoxedTypes", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(ClassWithAllBoxedTypes.class) + .build()).createCollectionAsync().block(); + + collection.createCollectionAsync().block(); + + return collection; + } + + + private static Stream supportedDataTypes() { + return Stream.of( + Arguments.of( + new ClassWithAllBoxedTypes( + "ID1", true, (byte) 127, (short) 3, 321, 5L, + 3.14f, 3.14159265358d, new BigDecimal("12345.67"), + OffsetDateTime.now(), UUID.randomUUID(), "abc".getBytes(StandardCharsets.UTF_8), + Arrays.asList(1.0f, 2.6f), + new Float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllBoxedTypes( + "ID2", false, Byte.MIN_VALUE, Short.MIN_VALUE, Integer.MIN_VALUE, Long.MIN_VALUE, + Float.MIN_VALUE, MIN_DOUBLE, BigDecimal.valueOf(MIN_DECIMAL), + OffsetDateTime.now(), UUID.randomUUID(), new byte[] {Byte.MIN_VALUE, -10, 0, 10, Byte.MAX_VALUE}, + Arrays.asList(Float.MIN_VALUE, -10f, 0f, 10f, Float.MAX_VALUE), + new Float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllBoxedTypes( + "ID3", false, Byte.MAX_VALUE, Short.MAX_VALUE, Integer.MAX_VALUE, Long.MAX_VALUE, + Float.MAX_VALUE, BIG_NUMBER.doubleValue(), BIG_NUMBER.subtract(BigDecimal.valueOf(0.01d)), + OffsetDateTime.now(), UUID.randomUUID(), null, + null, + new Float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllBoxedTypes( + "ID3", null, null, null, null, null, + null, null, null, + null, null, null, + null, + null + ) + ) + ); + } + + private static Stream supportedDataPrimitiveTypes() { + return Stream.of( + Arguments.of( + new ClassWithAllPrimitiveTypes( + "ID1", true, (byte) 127, (short) 3, 321, 5L, + 3.14f, 3.14159265358d, new BigDecimal("12345.67"), + OffsetDateTime.now(), UUID.randomUUID(), "abc".getBytes(StandardCharsets.UTF_8), + Arrays.asList(1.0f, 2.6f), + new float[]{0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f} + ) + ), + Arguments.of( + new ClassWithAllPrimitiveTypes( + "ID2", false, Byte.MIN_VALUE, Short.MIN_VALUE, Integer.MIN_VALUE, + Long.MIN_VALUE, + Float.MIN_VALUE, MIN_DOUBLE, BigDecimal.valueOf(MIN_DECIMAL), + OffsetDateTime.now(), UUID.randomUUID(), + new byte[]{Byte.MIN_VALUE, -10, 0, 10, Byte.MAX_VALUE}, + Arrays.asList(Float.MIN_VALUE, -10f, 0f, 10f, Float.MAX_VALUE), + new float[]{0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f} + ) + ), + Arguments.of( + new ClassWithAllPrimitiveTypes( + "ID3", false, Byte.MAX_VALUE, Short.MAX_VALUE, Integer.MAX_VALUE, + Long.MAX_VALUE, + Float.MAX_VALUE, BIG_NUMBER.doubleValue(), + BIG_NUMBER.subtract(BigDecimal.valueOf(0.01d)), + OffsetDateTime.now(), UUID.randomUUID(), null, + null, + new float[]{0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f} + ) + ), + Arguments.of( + new ClassWithAllPrimitiveTypes( + "ID3", false, (byte) 0, (short) 0, 0, 0l, + 0f, 0d, null, + null, null, null, + null, + null + ) + ) + ); + } +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreDataTypeTest.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreDataTypeTest.java new file mode 100644 index 000000000..eeb6a2e7b --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreDataTypeTest.java @@ -0,0 +1,234 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OracleVectorStoreDataTypeTest extends OracleCommonVectorStoreRecordCollectionTest { + private static final double MIN_NUMBER = 1.0E-130; + private static final BigDecimal BIG_NUMBER = BigDecimal.valueOf(9999999999999999.99); + + @ParameterizedTest + @MethodSource("supportedDataTypes") + void testDataTypes(ClassWithAllBoxedTypes values) { + + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE) + .build(); + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + VectorStoreRecordCollection collection = + vectorStore.getCollection("BoxedTypes", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(ClassWithAllBoxedTypes.class) + .build()).createCollectionAsync().block(); + + collection.createCollectionAsync().block(); + + ClassWithAllBoxedTypes record = values; + + collection.upsertAsync(record, null).block(); + + ClassWithAllBoxedTypes result = collection.getAsync(values.getId(), null).block(); + assertNotNull(result); + + assertEquals(values.getBooleanValue(), result.getBooleanValue()); + assertArrayEquals(values.getByteArrayValue(), result.getByteArrayValue()); + assertEquals(values.getByteValue(), result.getByteValue()); + assertEquals(values.getDoubleValue(), result.getDoubleValue()); + assertEquals(values.getFloatValue(), result.getFloatValue()); + assertEquals(values.getIntegerValue(), result.getIntegerValue()); + assertEquals(values.getListOfFloatValue(), result.getListOfFloatValue()); + assertEquals(values.getLongValue(), result.getLongValue()); + if (values.getOffsetDateTimeValue() != null) { + assertTrue(values.getOffsetDateTimeValue().isEqual(result.getOffsetDateTimeValue())); + } else { + assertTrue(result.getOffsetDateTimeValue() == null); + } + assertEquals(values.getShortValue(), result.getShortValue()); + assertEquals(values.getUuidValue(), result.getUuidValue()); + + collection.deleteCollectionAsync().block(); + } + + @ParameterizedTest + @MethodSource("supportedDataPrimitiveTypes") + void testPrimitiveDataTypes(ClassWithAllPrimitiveTypes values) { + + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE) + .build(); + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + VectorStoreRecordCollection collection = + vectorStore.getCollection("PrimitiveTypes", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(ClassWithAllPrimitiveTypes.class) + .build()).createCollectionAsync().block(); + + collection.createCollectionAsync().block(); + + ClassWithAllPrimitiveTypes record = values; + + collection.upsertAsync(record, null).block(); + + ClassWithAllPrimitiveTypes result = collection.getAsync(values.getId(), null).block(); + assertNotNull(result); + + assertEquals(values.getBooleanValue(), result.getBooleanValue()); + assertArrayEquals(values.getByteArrayValue(), result.getByteArrayValue()); + assertEquals(values.getByteValue(), result.getByteValue()); + assertEquals(values.getDoubleValue(), result.getDoubleValue()); + assertEquals(values.getFloatValue(), result.getFloatValue()); + assertEquals(values.getIntegerValue(), result.getIntegerValue()); + assertEquals(values.getListOfFloatValue(), result.getListOfFloatValue()); + assertEquals(values.getLongValue(), result.getLongValue()); + if (values.getOffsetDateTimeValue() != null) { + assertTrue(values.getOffsetDateTimeValue().isEqual(result.getOffsetDateTimeValue())); + } else { + assertTrue(result.getOffsetDateTimeValue() == null); + } + assertEquals(values.getShortValue(), result.getShortValue()); + assertEquals(values.getUuidValue(), result.getUuidValue()); + + collection.deleteCollectionAsync().block(); + } + + + private static Stream supportedDataTypes() { + return Stream.of( + Arguments.of( + new ClassWithAllBoxedTypes( + "ID1", true, (byte) 127, (short) 3, 321, 5L, + 3.14f, 3.14159265358d, new BigDecimal("12345.67"), + OffsetDateTime.now(), UUID.randomUUID(), "abc".getBytes(StandardCharsets.UTF_8), + Arrays.asList(1.0f, 2.6f), + new Float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllBoxedTypes( + "ID2", false, Byte.MIN_VALUE, Short.MIN_VALUE, Integer.MIN_VALUE, Long.MIN_VALUE, + Float.MIN_VALUE, MIN_NUMBER, BigDecimal.valueOf(MIN_NUMBER), + OffsetDateTime.now(), UUID.randomUUID(), new byte[] {Byte.MIN_VALUE, -10, 0, 10, Byte.MAX_VALUE}, + Arrays.asList(Float.MIN_VALUE, -10f, 0f, 10f, Float.MAX_VALUE), + new Float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllBoxedTypes( + "ID3", false, Byte.MAX_VALUE, Short.MAX_VALUE, Integer.MAX_VALUE, Long.MAX_VALUE, + Float.MAX_VALUE, BIG_NUMBER.doubleValue(), BIG_NUMBER.subtract(BigDecimal.valueOf(0.01d)), + OffsetDateTime.now(), UUID.randomUUID(), null, + null, + new Float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllBoxedTypes( + "ID3", null, null, null, null, null, + null, null, null, + null, null, null, + null, + null + ) + ) + ); + } + + private static Stream supportedDataPrimitiveTypes() { + return Stream.of( + Arguments.of( + new ClassWithAllPrimitiveTypes( + "ID1", true, (byte) 127, (short) 3, 321, 5L, + 3.14f, 3.14159265358d, new BigDecimal("12345.67"), + OffsetDateTime.now(), UUID.randomUUID(), "abc".getBytes(StandardCharsets.UTF_8), + Arrays.asList(1.0f, 2.6f), + new float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllPrimitiveTypes( + "ID2", false, Byte.MIN_VALUE, Short.MIN_VALUE, Integer.MIN_VALUE, Long.MIN_VALUE, + Float.MIN_VALUE, MIN_NUMBER, BigDecimal.valueOf(MIN_NUMBER), + OffsetDateTime.now(), UUID.randomUUID(), new byte[] {Byte.MIN_VALUE, -10, 0, 10, Byte.MAX_VALUE}, + Arrays.asList(Float.MIN_VALUE, -10f, 0f, 10f, Float.MAX_VALUE), + new float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllPrimitiveTypes( + "ID3", false, Byte.MAX_VALUE, Short.MAX_VALUE, Integer.MAX_VALUE, Long.MAX_VALUE, + Float.MAX_VALUE, BIG_NUMBER.doubleValue(), BIG_NUMBER.subtract(BigDecimal.valueOf(0.01d)), + OffsetDateTime.now(), UUID.randomUUID(), null, + null, + new float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f } + ) + ), + Arguments.of( + new ClassWithAllPrimitiveTypes( + "ID3", false, (byte)0, (short)0, 0, 0l, + 0f, 0d, null, + null, null, null, + null, + null + ) + ) + ); + } + +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreExtendedTest.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreExtendedTest.java new file mode 100644 index 000000000..af2e28fd8 --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreExtendedTest.java @@ -0,0 +1,502 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.oracle.OracleVectorStoreQueryProvider.StringTypeMapping; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; + +import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OracleVectorStoreExtendedTest extends OracleCommonVectorStoreRecordCollectionTest { + + // Test vector types + @Test + void testUseStringVec() { + VectorStoreRecordCollection collection = + createCollection( + "use_string_vec", + DummyRecordForVecString.class, + null); + + DummyRecordForVecString d1 = new DummyRecordForVecString("id1", "description1", "[1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8]"); + DummyRecordForVecString d2 = new DummyRecordForVecString("id2", "description2", "[1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8]"); + + collection.upsertBatchAsync(Arrays.asList(d1,d2), null).block(); + + DummyRecordForVecString rec = collection.getAsync("id1", + GetRecordOptions.builder().includeVectors(true).build()).block(); + + assertNotNull(rec); + assertEquals("[1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8]", rec.getVec()); + + collection.deleteCollectionAsync().block(); + } + + @Test + void testUseCollectionVec() { + VectorStoreRecordCollection collection = + createCollection( + "use_collection_vec", + DummyRecordForVecCollection.class, + null); + + List v1 = Arrays.asList(0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10f, -1.3f, 5.5f); + List v2 = Arrays.asList(-2f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f); + DummyRecordForVecCollection d1 = new DummyRecordForVecCollection("id1", "", v1); + DummyRecordForVecCollection d2 = new DummyRecordForVecCollection("id2", "", v2); + + collection.upsertBatchAsync(Arrays.asList(d1,d2), null).block(); + + DummyRecordForVecCollection rec = collection.getAsync("id1", + GetRecordOptions.builder().includeVectors(true).build()).block(); + + assertNotNull(rec); + assertEquals(8, rec.getVec().size()); + assertIterableEquals(v1, rec.getVec()); + + collection.deleteCollectionAsync().block(); + } + + // Test corner-case + @Test + void testUseCLOB() { + VectorStoreRecordCollection collection = + createCollection( + "use_clob", + DummyRecordForCLOB.class, + OracleVectorStoreQueryProvider.StringTypeMapping.USE_CLOB); + + DummyRecordForCLOB d1 = new DummyRecordForCLOB("id1", "clob-description", null); + DummyRecordForCLOB d2 = new DummyRecordForCLOB("id2", "clob-description2", vec(0)); + + collection.upsertBatchAsync(Arrays.asList(d1,d2), null).block(); + + try (Connection c = DATA_SOURCE.getConnection()) { + PreparedStatement st = c.prepareStatement( + "SELECT DATA_TYPE FROM USER_TAB_COLUMNS " + + "WHERE TABLE_NAME = 'SKCOLLECTION_USE_CLOB' AND COLUMN_NAME = 'DESCRIPTION'" + ); + ResultSet rs = st.executeQuery(); + rs.next(); + assertEquals("CLOB", rs.getString(1)); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + collection.deleteCollectionAsync().block(); + } + } + + @Test + void testClobLongText() { + VectorStoreRecordCollection collection = + createCollection( + "clob_long_text", + DummyRecordForCLOB.class, + OracleVectorStoreQueryProvider.StringTypeMapping.USE_CLOB); + + String longText = String.join("", java.util.Collections.nCopies(6000, "a")); + DummyRecordForCLOB r = new DummyRecordForCLOB("big", longText, vec(0)); + collection.upsertAsync(r, null).block(); + + DummyRecordForCLOB out = collection.getAsync("big", null).block(); + assertEquals(longText.length(), out.getDescription().length()); + assertTrue(out.getDescription().startsWith("aaaa")); + + collection.deleteCollectionAsync().block(); + } + + @Test + void testMultipleFilter() { + VectorStoreRecordCollection collection = + createCollection( + "multiple_filter", + DummyRecordForMultipleFilter.class, + null); + + DummyRecordForMultipleFilter d1 = new DummyRecordForMultipleFilter("id1", 4, 120, floatVec(0f)); + DummyRecordForMultipleFilter d2 = new DummyRecordForMultipleFilter("id2", 4, 100, floatVec(0f)); + DummyRecordForMultipleFilter d3 = new DummyRecordForMultipleFilter("id3", 3, 100, floatVec(0f)); + + collection.upsertBatchAsync(Arrays.asList(d1,d2,d3), null).block(); + + VectorSearchFilter filter = VectorSearchFilter.builder() + .equalTo("price",100) + .equalTo("stars", 4) + .build(); + + VectorSearchResults results = + collection.searchAsync(null, + VectorSearchOptions.builder() + .withVectorSearchFilter(filter) + .build() + ).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals("id2", results.getResults().get(0).getRecord().getId()); + + collection.deleteCollectionAsync().block(); + } + + @Test + void testVectorDimensionMismatch() { + VectorStoreRecordCollection collection = + createCollection( + "vector_dimension_mismatch", + DummyRecord.class, + null); + + // Empty vector rejected + DummyRecord d1 = new DummyRecord("id1", 4, 120d, new float[]{}); + SKException ex = assertThrows(SKException.class, + () -> collection.upsertBatchAsync(Arrays.asList(d1), null).block()); + assertTrue(ex.getCause().getMessage().contains("ORA-51803")); + + // Vector dimension mismatch + DummyRecord d2 = new DummyRecord("id1", 4, 120d, new float[]{1.1f,2.2f,3.3f,4.4f,5.5f}); + SKException ex2 = assertThrows(SKException.class, + () -> collection.upsertBatchAsync(Arrays.asList(d2), null).block()); + assertTrue(ex2.getCause().getMessage().contains("ORA-51803")); + + collection.deleteCollectionAsync().block(); + } + + @Test + void testNullFieldValue() { + VectorStoreRecordCollection collection = + createCollection("test_null", DummyRecord.class, null); + + DummyRecord d1 = new DummyRecord("id1", 4, null, floatVec(1)); + collection.upsertBatchAsync(Arrays.asList(d1), null).block(); + + VectorSearchFilter filter = VectorSearchFilter.builder() + .equalTo("price",null)// + .build(); + + VectorSearchResults results = collection.searchAsync( + null, + VectorSearchOptions.builder() + .withVectorSearchFilter(filter) + .build() + ).block(); + + assertEquals(1, results.getTotalCount()); + assertEquals("id1", results.getResults().get(0).getRecord().getId()); + + collection.deleteCollectionAsync().block(); + } + + @Test + void testSkipAndTop() { + VectorStoreRecordCollection collection = + createCollection( + "test_skip_and_top", + DummyRecord.class, + null); + + List l1 = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + l1.add(new DummyRecord("id" + i, i, (double) i, floatVec(i))); + } + collection.upsertBatchAsync(l1, null).block(); + + VectorSearchResults results = collection.searchAsync( + Collections.nCopies(8,0f), + VectorSearchOptions.builder() + .withIncludeVectors(true) + .withSkip(5) + .withTop(3) + .build() + ).block(); + + assertEquals(3, results.getResults().size()); + List ids = results.getResults().stream().map(r -> r.getRecord().getId()).collect( + Collectors.toList()); + assertEquals(Arrays.asList("id6","id7","id8"), ids); + + collection.deleteCollectionAsync().block(); + } + + // corner case for OracleVectorStoreRecordMapper + @Test + void testMapRecordToStorageModel_throws() { + VectorStoreRecordKeyField keyField = VectorStoreRecordKeyField.builder() + .withName("id") + .withStorageName("id") + .withFieldType(String.class) + .build(); + + VectorStoreRecordDefinition definition = + VectorStoreRecordDefinition.fromFields( + Arrays.asList(keyField) + ); + + OracleVectorStoreRecordMapper mapper = + OracleVectorStoreRecordMapper. builder() + .withRecordClass(DummyRecord.class) + .withVectorStoreRecordDefinition(definition) + .build(); + + UnsupportedOperationException ex = assertThrows( + UnsupportedOperationException.class, + () -> mapper.mapRecordToStorageModel(new DummyRecord())); + assertEquals("Not implemented", ex.getMessage()); + } + + private VectorStoreRecordCollection createCollection( + String collectionName, + Class recordClass, + OracleVectorStoreQueryProvider.StringTypeMapping stringTypeMapping) { + + OracleVectorStoreQueryProvider.Builder builder = + OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE); + + if (stringTypeMapping != null) { + builder.withStringTypeMapping(stringTypeMapping); + } + OracleVectorStoreQueryProvider queryProvider = builder.build(); + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + VectorStoreRecordCollection collection = + vectorStore.getCollection(collectionName, + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(recordClass) + .build()).createCollectionAsync().block(); + + return collection; + } + + private List vec(float x) { + return Arrays.asList(x, x+1, x+2, x+3, x+4, x+5, x+6, x+7); + } + + private float[] floatVec(float x) { + return new float[] { x, x+1, x+2, x+3, x+4, x+5, x+6, x+7 }; + } + + private static class DummyRecordForVecString { + @VectorStoreRecordKey + private final String id; + + @VectorStoreRecordData(isFilterable = false) + private final String description; + + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE, indexKind = IndexKind.IVFFLAT) + private final String vec; + + public DummyRecordForVecString() { + this(null, null, null); + } + public DummyRecordForVecString(String id, String description, String vec) { + this.id = id; + this.description = description; + this.vec = vec; + } + + public String getId() { + return id; + } + public String getDescription() { + return description; + } + public String getVec() { + return vec; + } + } + + private static class DummyRecordForVecCollection{ + @VectorStoreRecordKey + private String id; + + @VectorStoreRecordData(isFilterable = false) + private String description; + + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE, indexKind = IndexKind.IVFFLAT) + private Collection vec; + + public DummyRecordForVecCollection() { + this(null, null, null); + } + public DummyRecordForVecCollection(String id, String description, Collection vec) { + this.id = id; + this.description = description; + this.vec = vec; + } + + public String getId() { + return id; + } + public String getDescription() { + return description; + } + public Collection getVec() { + return vec; + } + } + + private static class DummyRecordForCLOB { + @VectorStoreRecordKey + private String id; + + @VectorStoreRecordData(isFilterable = false) + private String description; + + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE, indexKind = IndexKind.IVFFLAT) + private List vec; + + private DummyRecordForCLOB() { + this(null, null, null); + } + private DummyRecordForCLOB(String id, String description, List vec) { + this.id = id; + this.description = description; + this.vec = vec; + } + + public String getId() { + return id; + } + public String getDescription() { + return description; + } + public List getVec() { + return vec; + } + } + + private static class DummyRecordForMultipleFilter { + @VectorStoreRecordKey + private String id; + + @VectorStoreRecordData(isFilterable = true) + private int stars; + + @VectorStoreRecordData(isFilterable = true) + private double price; + + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE, indexKind = IndexKind.IVFFLAT) + private float[] vec; + + public DummyRecordForMultipleFilter() { + this(null, 0, 0d, null); + } + + public DummyRecordForMultipleFilter(String id, int stars, double price, float[] vec) { + this.id = id; + this.stars = stars; + this.price = price; + this.vec = vec; + } + + public String getId() { + return id; + } + public int getStars() { + return stars; + } + public double getPrice() { + return price; + } + public float[] getVec() { + return vec; + } + } + + private static class DummyRecord { + @VectorStoreRecordKey + private String id; + + @VectorStoreRecordData(isFilterable = true) + private int stars; + + @VectorStoreRecordData(isFilterable = true) + private Double price; + + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE, indexKind = IndexKind.IVFFLAT) + private float[] vec; + + public DummyRecord() { + this(null, 0, 0d, null); + } + + public DummyRecord(String id, int stars, Double price, float[] vec) { + this.id = id; + this.stars = stars; + this.price = price; + this.vec = vec; + } + + public String getId() { + return id; + } + public int getStars() { + return stars; + } + public Double getPrice() { + return price; + } + public float[] getVec() { + return vec; + } + } +} diff --git a/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreRecordCollectionTest.java b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreRecordCollectionTest.java new file mode 100644 index 000000000..850ae9238 --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/java/com/microsoft/semantickernel/data/jdbc/oracle/OracleVectorStoreRecordCollectionTest.java @@ -0,0 +1,592 @@ +package com.microsoft.semantickernel.data.jdbc.oracle; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OracleVectorStoreRecordCollectionTest extends OracleCommonVectorStoreRecordCollectionTest { + private static VectorStoreRecordCollection recordCollection; + + @BeforeAll + public static void setup() throws Exception { + + // Build a query provider + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE) + .build(); + + // Build a vector store + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + // Get a collection from the vector store + recordCollection = + vectorStore.getCollection("skhotels", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + + recordCollection.createCollectionIfNotExistsAsync().block(); + } + + @BeforeEach + public void clearCollection() { + recordCollection.deleteCollectionAsync().block(); + recordCollection.createCollectionAsync().block(); + } + + private static List getHotels() { + List vec1 = Arrays.asList(0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f); + float[] arrayf1 = new float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f }; + Float[] arrayF1 = new Float[] { 0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f }; + List vec2 = Arrays.asList(-2.0f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f); + float[] arrayf2 = new float[] { -2.0f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f }; + Float[] arrayF2 = new Float[] { -2.0f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f }; + List vec3 = Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.3f); + float[] arrayf3 = new float[] { 4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.3f }; + Float[] arrayF3 = new Float[] { 4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.3f }; + List vec4 = Arrays.asList(7.0f, 1.2f, -5.3f, 2.5f, 6.6f, -7.8f, 3.9f, -0.1f); + float[] arrayf4 = new float[] { 7.0f, 1.2f, -5.3f, 2.5f, 6.6f, -7.8f, 3.9f, -0.1f }; + Float[] arrayF4 = new Float[] { 7.0f, 1.2f, -5.3f, 2.5f, 6.6f, -7.8f, 3.9f, -0.1f }; + List vec5 =Arrays.asList(-3.5f, 4.4f, -1.2f, 9.9f, 5.7f, -6.1f, 7.8f, -2.0f); + float[] arrayf5 = new float[] { -3.5f, 4.4f, -1.2f, 9.9f, 5.7f, -6.1f, 7.8f, -2.0f }; + Float[] arrayF5 = new Float[] { -3.5f, 4.4f, -1.2f, 9.9f, 5.7f, -6.1f, 7.8f, -2.0f }; + return Arrays.asList( + new Hotel("id_1", "Hotel 1", 1, 1.49d, Arrays.asList("one", "two"), "Hotel 1 description", + vec1, arrayf1, arrayf1, arrayF1, + 4.0), + new Hotel("id_2", "Hotel 2", 2, 1.44d, Arrays.asList("three", "four"), "Hotel 2 description with free-text search", + vec2, arrayf2, arrayf2, arrayF2, + 4.0), + new Hotel("id_3", "Hotel 3", 3, 1.53d, Arrays.asList("five", "six"), "Hotel 3 description", + vec3, arrayf3, arrayf3, arrayF3, + 5.0), + new Hotel("id_4", "Hotel 4", 4, 1.35d, Arrays.asList("seven", "eight"), "Hotel 4 description", + vec4, arrayf4, arrayf4, arrayF4, + 4.0), + new Hotel("id_5", "Hotel 5", 5, 1.89d, Arrays.asList("nine", "ten"),"Hotel 5 description", + vec5, arrayf5, arrayf5, arrayF5, + 4.0)); + } + + /** + * Search embeddings similar to the third hotel embeddings. + * In order of similarity: + * 1. Hotel 3 + * 2. Hotel 1 + * 3. Hotel 4 + */ + private static final List SEARCH_EMBEDDINGS = Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, + -0.8f, 1.1f, -2.2f, 8.2f); + + @Test + public void createAndDeleteCollectionAsync() { + assertEquals(true, recordCollection.collectionExistsAsync().block()); + + recordCollection.deleteCollectionAsync().block(); + assertEquals(false, recordCollection.collectionExistsAsync().block()); + + recordCollection.createCollectionAsync().block(); + assertEquals(true, recordCollection.collectionExistsAsync().block()); + } + + @Test + public void upsertRecordAsync() { + List hotels = getHotels(); + for (Hotel hotel : hotels) { + recordCollection.upsertAsync(hotel, null).block(); + } + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNotNull(retrievedHotel); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertEquals(hotel.getName(), retrievedHotel.getName()); + assertEquals(hotel.getDescription(), retrievedHotel.getDescription()); + } + } + + @Test + public void upsertBatchAsync() { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + for (Hotel hotel : hotels) { + Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block(); + assertNotNull(retrievedHotel); + assertEquals(hotel.getId(), retrievedHotel.getId()); + assertEquals(hotel.getName(), retrievedHotel.getName()); + assertEquals(hotel.getDescription(), retrievedHotel.getDescription()); + } + } + + @Test + public void getBatchAsync() { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List keys = hotels.stream().map(Hotel::getId).collect(Collectors.toList()); + List retrievedHotels = recordCollection.getBatchAsync(keys, null).block(); + + assertNotNull(retrievedHotels); + assertEquals(keys.size(), retrievedHotels.size()); + for (Hotel hotel : retrievedHotels) { + assertTrue(keys.contains(hotel.getId())); + } + } + + @Test + public void deleteRecordAsync() { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + for (Hotel hotel : hotels) { + recordCollection.deleteAsync(hotel.getId(), null).block(); + assertNull(recordCollection.getAsync(hotel.getId(), null).block()); + } + } + + @Test + public void deleteBatchAsync() { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + List keys = hotels.stream().map(Hotel::getId).collect(Collectors.toList()); + recordCollection.deleteBatchAsync(keys, null).block(); + + for (String key : keys) { + assertNull(recordCollection.getAsync(key, null).block()); + } + } + + @ParameterizedTest + @MethodSource("parametersExactSearch") + public void exactSearch(DistanceFunction distanceFunction, List expectedDistance) { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(distanceFunction.getValue()) + .withTop(3) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection + .searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The third hotel should be the most similar + assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId()); + assertEquals(expectedDistance.get(0).doubleValue(), results.get(0).getScore(), 0.0002d); + assertEquals(hotels.get(0).getId(), results.get(1).getRecord().getId()); + assertEquals(expectedDistance.get(1).doubleValue(), results.get(1).getScore(), 0.0002d); + assertEquals(hotels.get(3).getId(), results.get(2).getRecord().getId()); + assertEquals(expectedDistance.get(2).doubleValue(), results.get(2).getScore(), 0.0002d); + + options = VectorSearchOptions.builder() + .withVectorFieldName(distanceFunction.getValue()) + .withSkip(1) + .withTop(-100) + .build(); + + // Skip the first result + results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(1, results.size()); + // The first hotel should be the most similar + assertEquals(hotels.get(0).getId(), results.get(0).getRecord().getId()); + assertEquals(results.get(0).getScore(), expectedDistance.get(1), 0.001d); + } + + @ParameterizedTest + @MethodSource("distanceFunctionAndDistance") + public void searchWithFilter(DistanceFunction distanceFunction, double expectedDistance) { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(distanceFunction.getValue()) + .withTop(3) + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("rating", 4.0).build()) + .build(); + + // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned + List> results = recordCollection + .searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The first hotel should be the most similar + assertEquals(hotels.get(0).getId(), results.get(0).getRecord().getId()); + assertEquals(results.get(0).getScore(), expectedDistance, 0.0002d); + } + + + @Test + public void searchWithTagFilter() { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() +// .withVectorFieldName("") + .withTop(3) + .withVectorSearchFilter( + VectorSearchFilter.builder() + .anyTagEqualTo("tags", "three") + .build()) + .build(); + + // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned + List> results = recordCollection + .searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(1, results.size()); + // The second hotel contains the tag we are searching for + assertEquals(hotels.get(1).getId(), results.get(0).getRecord().getId()); + } + + @ParameterizedTest + @MethodSource("supportedKeyTypes") + void testKeyTypes(String suffix, Class keyType, Object keyValue) { + VectorStoreRecordKeyField keyField = VectorStoreRecordKeyField.builder() + .withName("id") + .withStorageName("id") + .withFieldType(keyType) + .build(); + + VectorStoreRecordDataField dummyField = VectorStoreRecordDataField.builder() + .withName("dummy") + .withStorageName("dummy") + .withFieldType(String.class) + .build(); + + VectorStoreRecordVectorField dummyVector = VectorStoreRecordVectorField.builder() + .withName("vec") + .withStorageName("vec") + .withFieldType(List.class) + .withDimensions(2) + .withDistanceFunction(DistanceFunction.EUCLIDEAN_DISTANCE) + .withIndexKind(IndexKind.UNDEFINED) + .build(); + + VectorStoreRecordDefinition definition = VectorStoreRecordDefinition.fromFields( + Arrays.asList(keyField, dummyField, dummyVector) + ); + + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE) + .build(); + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + String collectionName = "test_keytype_" + suffix; + + VectorStoreRecordCollection collectionRaw = + vectorStore.getCollection(collectionName, + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(DummyRecordForKeyTypes.class) + .withRecordDefinition(definition) + .build()); + + VectorStoreRecordCollection collection = + (VectorStoreRecordCollection) collectionRaw; + + collection.createCollectionAsync().block(); + + DummyRecordForKeyTypes record = new DummyRecordForKeyTypes(keyValue, "dummyValue", Arrays.asList(1.0f, 2.0f)); + collection.upsertAsync(record, null).block(); + + DummyRecordForKeyTypes result = collection.getAsync(keyValue, null).block(); + assertNotNull(result); + assertEquals("dummyValue", result.getDummy()); + + collection.deleteCollectionAsync().block(); + } + + + @Nested + class HNSWIndexTests { + @Test + void testHNSWIndexIsCreatedSuccessfully() throws Exception { + VectorStoreRecordKeyField keyField = VectorStoreRecordKeyField.builder() + .withName("id") + .withStorageName("id") + .withFieldType(String.class) + .build(); + + VectorStoreRecordDataField dummyField = VectorStoreRecordDataField.builder() + .withName("dummy") + .withStorageName("dummy") + .withFieldType(String.class) + .isFilterable(false) + .build(); + + VectorStoreRecordVectorField hnswVector= VectorStoreRecordVectorField.builder() + .withName("hnsw") + .withStorageName("hnsw") + .withFieldType(List.class) + .withDimensions(8) + .withDistanceFunction(DistanceFunction.COSINE_SIMILARITY) + .withIndexKind(IndexKind.HNSW) + .build(); + + VectorStoreRecordDefinition definition = VectorStoreRecordDefinition.fromFields( + Arrays.asList(keyField, dummyField, hnswVector) + ); + + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE) + .build(); + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + String collectionName = "skhotels_hnsw"; + VectorStoreRecordCollection collection = + vectorStore.getCollection(collectionName, + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Object.class) + .withRecordDefinition(definition) + .build()); + + // create collection + collection.createCollectionAsync().block(); + + String expectedIndexName = hnswVector.getEffectiveStorageName().toUpperCase() + "_VECTOR_INDEX"; + + // check if index exist + try (Connection conn = DATA_SOURCE.getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "SELECT COUNT(*) FROM USER_INDEXES WHERE INDEX_NAME=?")) { + stmt.setString(1, expectedIndexName); + ResultSet rs = stmt.executeQuery(); + rs.next(); + int count = rs.getInt(1); + + assertEquals(1, count, "hnsw vector index should have been created"); + } finally { + // clean up + try (Connection conn = DATA_SOURCE.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate("DROP TABLE " + "SKCOLLECTION_" + collectionName); + } + } + } + } + + @Nested + class UndefinedIndexTests { + @Test + void testNoIndexIsCreatedForUndefined() throws Exception { + // create key field + VectorStoreRecordKeyField keyField = VectorStoreRecordKeyField.builder() + .withName("id") + .withStorageName("id") + .withFieldType(String.class) + .build(); + + // create vector field, set IndexKind to UNDEFINED + VectorStoreRecordVectorField undefinedVector= VectorStoreRecordVectorField.builder() + .withName("undef") + .withStorageName("undef") + .withFieldType(List.class) + .withDimensions(8) + .withDistanceFunction(DistanceFunction.COSINE_SIMILARITY) + .withIndexKind(IndexKind.UNDEFINED) + .build(); + + VectorStoreRecordDataField dummyField = VectorStoreRecordDataField.builder() + .withName("dummy") + .withStorageName("dummy") + .withFieldType(String.class) + .isFilterable(false) + .build(); + + VectorStoreRecordDefinition definition = VectorStoreRecordDefinition.fromFields( + Arrays.asList(keyField, dummyField, undefinedVector) + ); + + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(DATA_SOURCE) + .build(); + + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(DATA_SOURCE) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + String collectionName = "skhotels_undefined"; + VectorStoreRecordCollection collection = + vectorStore.getCollection(collectionName, + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Object.class) + .withRecordDefinition(definition) + .build()); + + // create collection + collection.createCollectionAsync().block(); + + // check if index exist + String expectedIndexName = undefinedVector.getEffectiveStorageName().toUpperCase() + "_VETCOR_INDEX"; + try (Connection conn = DATA_SOURCE.getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "SELECT COUNT(*) FROM USER_INDEXES WHERE INDEX_NAME = ?")) { + stmt.setString(1, expectedIndexName); + ResultSet rs = stmt.executeQuery(); + rs.next(); + int count = rs.getInt(1); + + assertEquals(0,count,"Vector index should not be created for IndexKind.UNDEFINED"); + } finally { + // clean up + try (Connection conn = DATA_SOURCE.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate("DROP TABLE " + "SKCOLLECTION_" + collectionName); + } + } + } + } + + private static Stream distanceFunctionAndDistance() { + return Stream.of( + Arguments.of (DistanceFunction.COSINE_DISTANCE, 0.8548d), + Arguments.of (DistanceFunction.COSINE_SIMILARITY, 0.1451d), + Arguments.of (DistanceFunction.DOT_PRODUCT, 30.3399d), + Arguments.of (DistanceFunction.EUCLIDEAN_DISTANCE, 18.9081d), + Arguments.of (DistanceFunction.UNDEFINED, 18.9081d) + ); + } + + private static Stream parametersExactSearch() { + return Stream.of( + Arguments.of (DistanceFunction.COSINE_SIMILARITY, Arrays.asList(0.9999d, 0.1451d, 0.0178d)), + Arguments.of (DistanceFunction.COSINE_DISTANCE, Arrays.asList(1.6422E-5d, 0.8548d, 0.9821d)), + Arguments.of (DistanceFunction.DOT_PRODUCT, Arrays.asList(202.3399d, 30.3399d, 3.6199d)), + Arguments.of (DistanceFunction.EUCLIDEAN_DISTANCE, Arrays.asList(0.1000d, 18.9081d, 19.9669d)), + Arguments.of (DistanceFunction.UNDEFINED, Arrays.asList(0.1000d, 18.9081d, 19.9669d)) + ); + } + + // commented out temporarily because only String type key is supported in + // JDBCVectorStoreRecordCollection#getKeyFromRecord: + // ... + // return (String) keyField.get(data); + // ... + // thus upsertAync/getAsync won't work + private static Stream supportedKeyTypes() { + return Stream.of( + Arguments.of("string", String.class, "asd123") /*, + Arguments.of("integer", Integer.class, 321), + Arguments.of("long", Long.class, 5L), + Arguments.of("short", Short.class, (short) 3), + Arguments.of("uuid", UUID.class, UUID.randomUUID())*/ + ); + } + + private static class DummyRecordForKeyTypes { + private final Object id; + private final String dummy; + private final List vec; + @JsonCreator + public DummyRecordForKeyTypes( + @JsonProperty("id")Object id, + @JsonProperty("dummy") String dummy, + @JsonProperty("vec") List vec) { + this.id = id; + this.dummy = dummy; + this.vec = vec; + } + + public Object getId() { + return id; + } + + public String getDummy() { + return dummy; + } + + @Override + public String toString() { + return String.valueOf(id); + } + } + + private static class DummyRecordForDataTypes { + private final String id; + private final Object dummy; + private final List vec; + @JsonCreator + public DummyRecordForDataTypes( + @JsonProperty("id") String id, + @JsonProperty("dummy") Object dummy, + @JsonProperty("vec") List vec) { + this.id = id; + this.dummy = dummy; + this.vec = vec; + } + + public String getId() { + return id; + } + + public Object getDummy() { + return dummy; + } + + @Override + public String toString() { + return String.valueOf(id); + } + } +} diff --git a/data/semantickernel-data-oracle/src/test/resources/initialize.sql b/data/semantickernel-data-oracle/src/test/resources/initialize.sql new file mode 100644 index 000000000..8756f121d --- /dev/null +++ b/data/semantickernel-data-oracle/src/test/resources/initialize.sql @@ -0,0 +1,12 @@ +-- Exit on any errors +WHENEVER SQLERROR EXIT SQL.SQLCODEAdd commentMore actions + +-- Configure the size of the Vector Pool to 1 GiB. +ALTER SYSTEM SET vector_memory_size=1G SCOPE=SPFILE; + +sqlplus / as sysdba + +SHUTDOWN ABORT; +STARTUP; + +exit \ No newline at end of file diff --git a/data/semantickernel-data-postgres/pom.xml b/data/semantickernel-data-postgres/pom.xml new file mode 100644 index 000000000..cfc838f72 --- /dev/null +++ b/data/semantickernel-data-postgres/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../../pom.xml + + + semantickernel-data-postgres + Semantic Kernel PostreSQL connector + Provides a PostreSQL connector for the Semantic Kernel + + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-data-jdbc + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + com.microsoft.semantic-kernel + semantickernel-api-data + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + + com.fasterxml.jackson.core + jackson-databind + compile + + + com.fasterxml.jackson.core + jackson-core + compile + + + com.github.spotbugs + spotbugs-annotations + + + org.postgresql + postgresql + 42.7.12 + + + \ No newline at end of file diff --git a/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorDistanceFunction.java b/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorDistanceFunction.java new file mode 100644 index 000000000..4ad2bd3b9 --- /dev/null +++ b/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorDistanceFunction.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc.postgres; + +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; + +/** + * Represents a PostgreSQL vector distance function. + */ +public enum PostgreSQLVectorDistanceFunction { + /** + * Euclidean L2 distance function. + */ + L2("vector_l2_ops", "<->"), + /** + * The cosine distance function. + */ + COSINE("vector_cosine_ops", "<=>"), + /** + * The inner product distance function. + */ + INNER_PRODUCT("vector_ip_ops", "<#>"), + /** + * The distance function is undefined. + */ + UNDEFINED(null, null); + + private final String value; + private final String operator; + + PostgreSQLVectorDistanceFunction(String value, String operator) { + this.value = value; + this.operator = operator; + } + + /** + * Gets the value of the distance function. + * @return the value of the distance function + */ + public String getValue() { + return value; + } + + /** + * Gets the operator of the distance function. + * @return the operator of the distance function + */ + public String getOperator() { + return operator; + } + + /** + * Converts a distance function to a PostgreSQL vector distance function. + * @param function the distance function + * @return the PostgreSQL vector distance function + */ + public static PostgreSQLVectorDistanceFunction fromDistanceFunction(DistanceFunction function) { + switch (function) { + case EUCLIDEAN_DISTANCE: + return L2; + case COSINE_DISTANCE: + return COSINE; + case DOT_PRODUCT: + return INNER_PRODUCT; + case UNDEFINED: + return UNDEFINED; + default: + throw new IllegalArgumentException("Unsupported distance function: " + function); + } + } +} diff --git a/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorIndexKind.java b/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorIndexKind.java new file mode 100644 index 000000000..cd0681b87 --- /dev/null +++ b/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorIndexKind.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc.postgres; + +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; + +/** + * Represents a PostgreSQL vector index kind. + */ +public enum PostgreSQLVectorIndexKind { + /** + * The vector is indexed using an HNSW algorithm. + */ + HNSW("hnsw"), + /** + * The vector is indexed using a Flat algorithm. + */ + IVFFLAT("ivfflat"), + /** + * The indexing algorithm is undefined. + */ + UNDEFINED(null); + + private final String value; + + PostgreSQLVectorIndexKind(String value) { + this.value = value; + } + + /** + * Gets the pgvector value of the index kind. + * @return the pgvector value of the index kind + */ + public String getValue() { + return value; + } + + /** + * Converts an index kind to a PostgreSQL vector index kind. + * @param indexKind the index kind + * @return the PostgreSQL vector index kind + */ + public static PostgreSQLVectorIndexKind fromIndexKind(IndexKind indexKind) { + switch (indexKind) { + case HNSW: + return HNSW; + case IVFFLAT: + return IVFFLAT; + case FLAT: + case UNDEFINED: + return UNDEFINED; + default: + throw new IllegalArgumentException("Unsupported index kind: " + indexKind); + } + } +} diff --git a/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorStoreQueryProvider.java b/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorStoreQueryProvider.java new file mode 100644 index 000000000..e65bb9eb1 --- /dev/null +++ b/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorStoreQueryProvider.java @@ -0,0 +1,518 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc.postgres; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.filter.AnyTagEqualToFilterClause; +import com.microsoft.semantickernel.data.filter.EqualToFilterClause; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.sql.DataSource; + +/** + * The MySQL vector store query provider. + * Provides the necessary methods to interact with a MySQL + * vector store and vector store collections. + */ +public class PostgreSQLVectorStoreQueryProvider extends + JDBCVectorStoreQueryProvider implements SQLVectorStoreQueryProvider { + private final String collectionsTable; + private final String prefixForCollectionTables; + private final ObjectMapper objectMapper; + + private PostgreSQLVectorStoreQueryProvider( + @Nonnull DataSource dataSource, + @Nonnull String collectionsTable, + @Nonnull String prefixForCollectionTables, + @Nonnull ObjectMapper objectMapper) { + super( + dataSource, + collectionsTable, + prefixForCollectionTables, + buildSupportedKeyTypes(), + buildSupportedDataTypes(), + buildSupportedVectorTypes()); + this.collectionsTable = collectionsTable; + this.prefixForCollectionTables = prefixForCollectionTables; + this.objectMapper = objectMapper; + } + + private static Map, String> buildSupportedVectorTypes() { + HashMap, String> supportedVectorTypes = new HashMap<>(); + supportedVectorTypes.put(String.class, "TEXT"); + supportedVectorTypes.put(List.class, "VECTOR(%d)"); + supportedVectorTypes.put(Collection.class, "VECTOR(%d)"); + return supportedVectorTypes; + } + + private static Map, String> buildSupportedDataTypes() { + HashMap, String> supportedDataTypes = new HashMap<>(); + supportedDataTypes.put(String.class, "TEXT"); + supportedDataTypes.put(Integer.class, "INTEGER"); + supportedDataTypes.put(int.class, "INTEGER"); + supportedDataTypes.put(Long.class, "BIGINT"); + supportedDataTypes.put(long.class, "BIGINT"); + supportedDataTypes.put(Float.class, "REAL"); + supportedDataTypes.put(float.class, "REAL"); + supportedDataTypes.put(Double.class, "DOUBLE PRECISION"); + supportedDataTypes.put(double.class, "DOUBLE PRECISION"); + supportedDataTypes.put(Boolean.class, "BOOLEAN"); + supportedDataTypes.put(boolean.class, "BOOLEAN"); + supportedDataTypes.put(OffsetDateTime.class, "TIMESTAMPTZ"); + supportedDataTypes.put(List.class, "JSONB"); + return supportedDataTypes; + } + + private static HashMap, String> buildSupportedKeyTypes() { + HashMap, String> supportedKeyTypes = new HashMap<>(); + supportedKeyTypes.put(String.class, "VARCHAR(255)"); + return supportedKeyTypes; + } + + /** + * Creates a new builder. + * @return the builder + */ + public static PostgreSQLVectorStoreQueryProvider.Builder builder() { + return new PostgreSQLVectorStoreQueryProvider.Builder(); + } + + /** + * Prepares the vector store. + * Executes any necessary setup steps for the vector store. + * + * @throws SKException if an error occurs while preparing the vector store + */ + @Override + public void prepareVectorStore() { + super.prepareVectorStore(); + + // Create the vector extension + String pgVector = "CREATE EXTENSION IF NOT EXISTS vector"; + + try (Connection connection = dataSource.getConnection(); + PreparedStatement createPgVector = connection.prepareStatement(pgVector)) { + createPgVector.execute(); + } catch (SQLException e) { + throw new SKException("Failed to prepare vector store", e); + } + } + + private String getColumnNamesAndTypesForVectorFields( + List fields) { + return fields.stream() + .map(field -> { + String columnType; + if (field.getFieldType().equals(String.class)) { + columnType = supportedVectorTypes.get(String.class); + } else { + // Get the vector type and dimensions + columnType = String.format(supportedVectorTypes.get(field.getFieldType()), + field.getDimensions()); + } + return validateSQLidentifier(field.getEffectiveStorageName()) + " " + columnType; + }) + .collect(Collectors.joining(", ")); + } + + private String createIndexForVectorField(String collectionName, + VectorStoreRecordVectorField vectorField) { + PostgreSQLVectorIndexKind indexKind = PostgreSQLVectorIndexKind + .fromIndexKind(vectorField.getIndexKind()); + PostgreSQLVectorDistanceFunction distanceFunction = PostgreSQLVectorDistanceFunction + .fromDistanceFunction(vectorField.getDistanceFunction()); + + // If there is no approximate search index associated to the vector field, + // there is no need to create an index and pgvector performs exact nearest neighbor search. + if (indexKind == PostgreSQLVectorIndexKind.UNDEFINED) { + return null; + } + if (distanceFunction == PostgreSQLVectorDistanceFunction.UNDEFINED) { + throw new SKException( + "Distance function is required for vector field: " + vectorField.getName()); + } + + return formatQuery("CREATE INDEX IF NOT EXISTS %s ON %s USING %s (%s %s);", + getCollectionTableName(collectionName) + "_index", + getCollectionTableName(collectionName), + indexKind.getValue(), + vectorField.getEffectiveStorageName(), + distanceFunction.getValue()); + } + + /** + * Creates a collection. + * + * @param collectionName the collection name + * @param recordDefinition the record definition + * @throws SKException if an error occurs while creating the collection + */ + @Override + @SuppressFBWarnings(value = { + "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE", + "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" + }) // SQL query is generated dynamically with valid identifiers + public void createCollection(String collectionName, + VectorStoreRecordDefinition recordDefinition) { + + List vectorFields = recordDefinition.getVectorFields(); + + try (Connection connection = dataSource.getConnection(); + Statement createTableAndIndexes = connection.createStatement()) { + + String createStorageTable = formatQuery("CREATE TABLE IF NOT EXISTS %s (" + + "%s VARCHAR(255) PRIMARY KEY, " + + "%s, " + + "%s);", + getCollectionTableName(collectionName), + getKeyColumnName(recordDefinition.getKeyField()), + getColumnNamesAndTypes(new ArrayList<>(recordDefinition.getDataFields()), + supportedDataTypes), + getColumnNamesAndTypesForVectorFields(recordDefinition.getVectorFields())); + + createTableAndIndexes.addBatch(createStorageTable); + for (VectorStoreRecordVectorField vectorField : vectorFields) { + String createVectorIndex = createIndexForVectorField(collectionName, vectorField); + + if (createVectorIndex != null) { + createTableAndIndexes.addBatch(createVectorIndex); + } + } + + createTableAndIndexes.executeBatch(); + } catch (SQLException e) { + throw new SKException("Failed to create collection", e); + } + + String insertCollectionQuery = formatQuery("INSERT INTO %s (collectionId) VALUES (?)", + validateSQLidentifier(collectionsTable)); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement insert = connection.prepareStatement(insertCollectionQuery)) { + insert.setObject(1, collectionName); + insert.execute(); + } catch (SQLException e) { + throw new SKException("Failed to insert collection", e); + } + } + + private void setUpsertStatementValues(PreparedStatement statement, Object record, + List fields) { + JsonNode jsonNode = objectMapper.valueToTree(record); + + for (int i = 0; i < fields.size(); ++i) { + VectorStoreRecordField field = fields.get(i); + try { + JsonNode valueNode = jsonNode.get(field.getEffectiveStorageName()); + + if (field instanceof VectorStoreRecordVectorField) { + // Convert the vector field to a string + if (!field.getFieldType().equals(String.class)) { + statement.setObject(i + 1, objectMapper.writeValueAsString(valueNode)); + continue; + } + } else if (field instanceof VectorStoreRecordDataField) { + // Convert List field to a string + if (field.getFieldType().equals(List.class)) { + statement.setObject(i + 1, objectMapper.writeValueAsString(valueNode)); + continue; + } + } + + statement.setObject(i + 1, + objectMapper.convertValue(valueNode, field.getFieldType())); + } catch (SQLException | JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + private String getWildcardStringWithCast(List fields) { + return fields.stream() + .map(field -> { + String wildcard = "?"; + // Add casting for vector fields + if (field instanceof VectorStoreRecordVectorField) { + wildcard += "::vector"; + } + if (field instanceof VectorStoreRecordDataField) { + // Add casting for List fields + if (field.getFieldType().equals(List.class)) { + wildcard += "::jsonb"; + } + } + return wildcard; + }) + .collect(Collectors.joining(", ")); + } + + /** + * Upserts records into the collection. + * @param collectionName the collection name + * @param records the records to upsert + * @param recordDefinition the record definition + * @param options the upsert options + * @throws SKException if the upsert fails + */ + @Override + @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") // SQL query is generated dynamically with valid identifiers + public void upsertRecords(String collectionName, List records, + VectorStoreRecordDefinition recordDefinition, UpsertRecordOptions options) { + validateSQLidentifier(getCollectionTableName(collectionName)); + List fields = recordDefinition.getAllFields(); + + String onDuplicateKeyUpdate = fields.stream() + .filter(field -> !(field instanceof VectorStoreRecordKeyField)) // Exclude key fields + .map(field -> formatQuery("%s = EXCLUDED.%s", + validateSQLidentifier(field.getEffectiveStorageName()), + field.getEffectiveStorageName())) + .collect(Collectors.joining(", ")); + + String query = formatQuery( + "INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s", + getCollectionTableName(collectionName), + getQueryColumnsFromFields(fields), + getWildcardStringWithCast(fields), + getKeyColumnName(recordDefinition.getKeyField()), + onDuplicateKeyUpdate); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + for (Object record : records) { + setUpsertStatementValues(statement, record, recordDefinition.getAllFields()); + statement.addBatch(); + } + + statement.executeBatch(); + } catch (SQLException e) { + throw new SKException("Failed to upsert records", e); + } + } + + /** + * Vector search. + * Executes a vector search query and returns the results. + * The results are mapped to the specified record type using the provided mapper. + * The query is executed against the specified collection. + * + * @param the record type + * @param collectionName the collection name + * @param vector the vector to search with + * @param options the search options + * @param recordDefinition the record definition + * @param mapper the mapper, responsible for mapping the result set to the record type. + * @return the search results + */ + @Override + public VectorSearchResults search(String collectionName, + List vector, VectorSearchOptions options, + VectorStoreRecordDefinition recordDefinition, + VectorStoreRecordMapper mapper) { + if (recordDefinition.getVectorFields().isEmpty()) { + throw new SKException("No vector fields defined. Cannot perform vector search"); + } + + VectorStoreRecordVectorField firstVectorField = recordDefinition.getVectorFields() + .get(0); + if (options == null) { + options = VectorSearchOptions.createDefault(firstVectorField.getName()); + } + + VectorStoreRecordVectorField vectorField = options.getVectorFieldName() == null + ? firstVectorField + : (VectorStoreRecordVectorField) recordDefinition + .getField(options.getVectorFieldName()); + + PostgreSQLVectorIndexKind indexKind = PostgreSQLVectorIndexKind + .fromIndexKind(vectorField.getIndexKind()); + PostgreSQLVectorDistanceFunction distanceFunction = PostgreSQLVectorDistanceFunction + .fromDistanceFunction(vectorField.getDistanceFunction()); + + // If there is no approximate search index associated to the vector field, + // there is no index defined in the database and pgvector performs exact nearest neighbor search. + // If indexKind is defined, distance function is required. + if (indexKind != PostgreSQLVectorIndexKind.UNDEFINED + && distanceFunction == PostgreSQLVectorDistanceFunction.UNDEFINED) { + throw new SKException( + "Distance function is required for vector field: " + vectorField.getName()); + } + + String filter = getFilter(options.getVectorSearchFilter(), recordDefinition); + List parameters = getFilterParameters(options.getVectorSearchFilter()); + + String filterClause = filter.isEmpty() ? "" : "WHERE " + filter; + String searchQuery = formatQuery( + "SELECT %s, %s %s ?::vector AS score FROM %s %s ORDER BY score LIMIT ? OFFSET ?", + getQueryColumnsFromFields( + options.isIncludeVectors() ? recordDefinition.getAllFields() + : recordDefinition.getNonVectorFields()), + validateSQLidentifier(vectorField.getEffectiveStorageName()), + distanceFunction == null ? PostgreSQLVectorDistanceFunction.L2.getOperator() + : distanceFunction.getOperator(), + getCollectionTableName(collectionName), + filterClause); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(searchQuery)) { + int parameterIndex = 1; + + statement.setString(parameterIndex++, + objectMapper.writeValueAsString(vector)); + for (Object parameter : parameters) { + statement.setObject(parameterIndex++, parameter); + } + statement.setInt(parameterIndex++, options.getTop()); + statement.setInt(parameterIndex, options.getSkip()); + + List> records = new ArrayList<>(); + ResultSet resultSet = statement.executeQuery(); + + while (resultSet.next()) { + records.add(new VectorSearchResult<>( + mapper.mapStorageModelToRecord(resultSet, + new GetRecordOptions(options.isIncludeVectors())), + resultSet.getDouble("score"))); + } + + return new VectorSearchResults<>(records); + } catch (SQLException | JsonProcessingException e) { + throw new SKException("Failed to search records", e); + } + } + + /** + * Gets the filter parameters for the given vector search filter to associate with the filter string + * generated by the getFilter method. + * + * @param filter The filter to get the filter parameters for. + * @return The filter parameters. + */ + @Override + public List getFilterParameters(VectorSearchFilter filter) { + if (filter == null + || filter.getFilterClauses().isEmpty()) { + return Collections.emptyList(); + } + + return filter.getFilterClauses().stream().map(filterClause -> { + if (filterClause instanceof EqualToFilterClause) { + EqualToFilterClause equalToFilterClause = (EqualToFilterClause) filterClause; + return equalToFilterClause.getValue(); + } else if (filterClause instanceof AnyTagEqualToFilterClause) { + AnyTagEqualToFilterClause anyTagEqualToFilterClause = (AnyTagEqualToFilterClause) filterClause; + return String.format("[\"%s\"]", anyTagEqualToFilterClause.getValue()); + } else { + throw new SKException("Unsupported filter clause type '" + + filterClause.getClass().getSimpleName() + "'."); + } + }).collect(Collectors.toList()); + } + + @Override + public String getAnyTagEqualToFilter(AnyTagEqualToFilterClause filterClause) { + String fieldName = JDBCVectorStoreQueryProvider + .validateSQLidentifier(filterClause.getFieldName()); + + return String.format("%s @> ?::jsonb", fieldName); + } + + @Override + public VectorStoreRecordMapper getVectorStoreRecordMapper( + Class recordClass, + VectorStoreRecordDefinition recordDefinition) { + return PostgreSQLVectorStoreRecordMapper.builder() + .withRecordClass(recordClass) + .withVectorStoreRecordDefinition(recordDefinition) + .build(); + } + + /** + * A builder for the PostgreSQLVectorStoreQueryProvider class. + */ + public static class Builder + extends JDBCVectorStoreQueryProvider.Builder { + private DataSource dataSource; + private String collectionsTable = DEFAULT_COLLECTIONS_TABLE; + private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; + private ObjectMapper objectMapper = new ObjectMapper(); + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public PostgreSQLVectorStoreQueryProvider.Builder withDataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + /** + * Sets the collections table name. + * @param collectionsTable the collections table name + * @return the builder + */ + public PostgreSQLVectorStoreQueryProvider.Builder withCollectionsTable( + String collectionsTable) { + this.collectionsTable = validateSQLidentifier(collectionsTable); + return this; + } + + /** + * Sets the prefix for collection tables. + * @param prefixForCollectionTables the prefix for collection tables + * @return the builder + */ + public PostgreSQLVectorStoreQueryProvider.Builder withPrefixForCollectionTables( + String prefixForCollectionTables) { + this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); + return this; + } + + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public PostgreSQLVectorStoreQueryProvider.Builder withObjectMapper( + ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + public PostgreSQLVectorStoreQueryProvider build() { + if (dataSource == null) { + throw new SKException("DataSource is required"); + } + + return new PostgreSQLVectorStoreQueryProvider(dataSource, collectionsTable, + prefixForCollectionTables, objectMapper); + } + } +} diff --git a/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorStoreRecordMapper.java b/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorStoreRecordMapper.java new file mode 100644 index 000000000..11ced0e13 --- /dev/null +++ b/data/semantickernel-data-postgres/src/main/java/com/microsoft/semantickernel/data/jdbc/postgres/PostgreSQLVectorStoreRecordMapper.java @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc.postgres; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.postgresql.util.PGobject; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.function.BiFunction; + +/** + * A mapper to convert between a record and a PostgreSQL storage model. + * + * @param the record type + */ +public class PostgreSQLVectorStoreRecordMapper + extends VectorStoreRecordMapper { + + /** + * Constructs a new instance of the VectorStoreRecordMapper. + * + * @param storageModelToRecordMapper the function to convert a storage model to a record + */ + protected PostgreSQLVectorStoreRecordMapper( + BiFunction storageModelToRecordMapper) { + super(null, storageModelToRecordMapper); + } + + /** + * Creates a new builder. + * + * @param the record type + * @return the builder + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * A builder for the PostgreSQLVectorStoreRecordMapper. + * + * @param the record type + */ + public static class Builder + implements SemanticKernelBuilder> { + private Class recordClass; + private VectorStoreRecordDefinition vectorStoreRecordDefinition; + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Sets the record class. + * + * @param recordClass the record class + * @return the builder + */ + public Builder withRecordClass(Class recordClass) { + this.recordClass = recordClass; + return this; + } + + /** + * Sets the vector store record definition. + * + * @param vectorStoreRecordDefinition the vector store record definition + * @return the builder + */ + public Builder withVectorStoreRecordDefinition( + VectorStoreRecordDefinition vectorStoreRecordDefinition) { + this.vectorStoreRecordDefinition = vectorStoreRecordDefinition; + return this; + } + + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Builds the {@link PostgreSQLVectorStoreRecordMapper}. + * + * @return the {@link PostgreSQLVectorStoreRecordMapper} + */ + public PostgreSQLVectorStoreRecordMapper build() { + if (recordClass == null) { + throw new SKException("recordClass is required"); + } + if (vectorStoreRecordDefinition == null) { + throw new SKException("vectorStoreRecordDefinition is required"); + } + + return new PostgreSQLVectorStoreRecordMapper<>( + (resultSet, options) -> { + try { + // Create an ObjectNode to hold the values + ObjectNode objectNode = objectMapper.createObjectNode(); + + // Select fields from the record definition. + List fields; + if (options != null && options.isIncludeVectors()) { + fields = vectorStoreRecordDefinition.getAllFields(); + } else { + fields = vectorStoreRecordDefinition.getNonVectorFields(); + } + + for (VectorStoreRecordField field : fields) { + Object value = resultSet.getObject(field.getEffectiveStorageName()); + Class fieldType = field.getFieldType(); + + if (field instanceof VectorStoreRecordVectorField) { + // If the vector field is other than String, deserialize it from the JSON string + if (!fieldType.equals(String.class)) { + // Deserialize the pgvector string to the vector type + value = objectMapper.readValue(((PGobject) value).getValue(), + fieldType); + } + } else if (field instanceof VectorStoreRecordDataField) { + // If the field is List, deserialize it from the JSON string + if (fieldType.equals(List.class)) { + value = objectMapper.readValue(((PGobject) value).getValue(), + fieldType); + } + } + + JsonNode genericNode = objectMapper.valueToTree(value); + objectNode.set(field.getEffectiveStorageName(), genericNode); + } + + return objectMapper.treeToValue(objectNode, recordClass); + } catch (SQLException | JsonProcessingException e) { + throw new SKException( + "Failure to serialize object, by default the JDBC connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", + e); + } + }); + } + } +} diff --git a/data/semantickernel-data-redis/pom.xml b/data/semantickernel-data-redis/pom.xml new file mode 100644 index 000000000..db151484b --- /dev/null +++ b/data/semantickernel-data-redis/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../../pom.xml + + + semantickernel-data-redis + Semantic Kernel Redis connector + Provides a Redis connector for the Semantic Kernel + + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-api-data + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.core + jackson-databind + compile + + + com.fasterxml.jackson.core + jackson-core + compile + + + com.github.jknack + handlebars + + + com.google.code.findbugs + jsr305 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + compile + + + com.github.spotbugs + spotbugs-annotations + + + org.apache.commons + commons-text + + + + redis.clients + jedis + + + + + org.junit.jupiter + junit-jupiter + test + + + + \ No newline at end of file diff --git a/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordCollection.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordCollection.java new file mode 100644 index 000000000..74dd1213b --- /dev/null +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordCollection.java @@ -0,0 +1,444 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorsearch.VectorizedSearch; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.options.DeleteRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.lang3.tuple.Pair; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import redis.clients.jedis.JedisPooled; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.Response; +import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.search.FTSearchParams; +import redis.clients.jedis.search.IndexDefinition; +import redis.clients.jedis.search.IndexOptions; +import redis.clients.jedis.search.Schema; +import redis.clients.jedis.search.SearchResult; + +import javax.annotation.Nonnull; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * RedisHashSetVectorStoreRecordCollection is a class that represents a + * collection of records stored in Redis using the Hash Set data structure. + * @param The record type. + */ +public class RedisHashSetVectorStoreRecordCollection + implements VectorStoreRecordCollection, + VectorizedSearch { + + private static final HashSet> supportedKeyTypes = new HashSet<>( + Collections.singletonList( + String.class)); + + private static final HashSet> supportedVectorTypes = new HashSet<>( + Arrays.asList( + List.class, + Collection.class)); + + private final JedisPooled client; + private final String collectionName; + private final RedisHashSetVectorStoreRecordCollectionOptions options; + private final VectorStoreRecordMapper>> vectorStoreRecordMapper; + private final VectorStoreRecordDefinition recordDefinition; + private final byte[][] dataFields; + private final ObjectMapper objectMapper; + + /** + * Creates a new instance of the RedisVectorRecordStore. + * + * @param client The Redis client. + * @param collectionName The name of the collection. + * @param options The options for the store. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public RedisHashSetVectorStoreRecordCollection( + @Nonnull JedisPooled client, + @Nonnull String collectionName, + @Nonnull RedisHashSetVectorStoreRecordCollectionOptions options) { + this.client = client; + this.collectionName = collectionName; + this.options = options; + + // If record definition is not provided, create one from the record class + if (options.getRecordDefinition() == null) { + this.recordDefinition = VectorStoreRecordDefinition.fromRecordClass( + options.getRecordClass()); + } else { + this.recordDefinition = options.getRecordDefinition(); + } + + // Validate supported types + VectorStoreRecordDefinition.validateSupportedTypes( + Collections.singletonList(recordDefinition.getKeyField()), + supportedKeyTypes); + VectorStoreRecordDefinition.validateSupportedTypes( + new ArrayList<>(recordDefinition.getVectorFields()), + supportedVectorTypes); + + // If mapper is not provided, set a default one + objectMapper = options.getObjectMapper() != null ? options.getObjectMapper() + : new ObjectMapper(); + + // If mapper is not provided, set a default one + if (options.getVectorStoreRecordMapper() == null) { + vectorStoreRecordMapper = new RedisHashSetVectorStoreRecordMapper.Builder() + .withRecordClass(options.getRecordClass()) + .withVectorStoreRecordDefinition(recordDefinition) + .withObjectMapper(objectMapper) + .build(); + } else { + vectorStoreRecordMapper = options.getVectorStoreRecordMapper(); + } + + // Creates a list of paths to retrieve from Redis when no vectors are requested + this.dataFields = new byte[recordDefinition.getDataFields().size()][]; + for (int i = 0; i < recordDefinition.getDataFields().size(); i++) { + this.dataFields[i] = stringToBytes( + recordDefinition.getDataFields().get(i).getEffectiveStorageName()); + } + } + + /** + * Gets the name of the collection. + * + * @return The name of the collection. + */ + @Override + public String getCollectionName() { + return collectionName; + } + + /** + * Checks if the collection exists in the store. + * + * @return A Mono emitting a boolean indicating if the collection exists. + */ + @Override + public Mono collectionExistsAsync() { + return Mono.fromCallable(() -> { + try { + Map info = this.client.ftInfo(collectionName); + return info != null && !info.isEmpty(); + } catch (Exception e) { + if (!(e instanceof JedisDataException)) { + throw e; + } + return false; + } + }).subscribeOn(Schedulers.boundedElastic()); + } + + /** + * Creates the collection in the store. + * + * @return A Mono representing the completion of the creation operation. + */ + @Override + public Mono> createCollectionAsync() { + return Mono.fromRunnable(() -> { + Schema schema = RedisVectorStoreCollectionCreateMapping + .mapToSchema(recordDefinition.getAllFields(), RedisStorageType.HASH_SET); + + IndexDefinition indexDefinition = new IndexDefinition(IndexDefinition.Type.HASH) + .setPrefixes(collectionName + ":"); + + client.ftCreate( + collectionName, + IndexOptions.defaultOptions().setDefinition(indexDefinition), + schema); + }) + .subscribeOn(Schedulers.boundedElastic()) + .then(Mono.just(this)); + } + + /** + * Creates the collection in the store if it does not exist. + * + * @return A Mono representing the completion of the creation operation. + */ + @Override + public Mono> createCollectionIfNotExistsAsync() { + return collectionExistsAsync().flatMap(exists -> { + if (!exists) { + return createCollectionAsync(); + } + + return Mono.just(this); + }); + } + + /** + * Deletes the collection from the store. + * + * @return A Mono representing the completion of the deletion operation. + */ + @Override + public Mono deleteCollectionAsync() { + return Mono.fromRunnable(() -> client.ftDropIndex(collectionName)) + .subscribeOn(Schedulers.boundedElastic()) + .then(); + } + + private String prefixKeyIfNeeded(String key, String collectionName) { + return options.isPrefixCollectionName() ? collectionName + ":" + key : key; + } + + private String removeKeyPrefixIfNeeded(String key, String collectionName) { + if (options.isPrefixCollectionName() && key.startsWith(collectionName + ":")) { + return key.substring(collectionName.length() + 1); + } + return key; + } + + static byte[] stringToBytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private Map addDataFieldNames(List result) { + Map dataFields = new HashMap<>(); + for (int i = 0; i < result.size(); i++) { + if (result.get(i) == null) { + continue; + } + dataFields.put(this.dataFields[i], result.get(i)); + } + return dataFields; + } + + /** + * Gets a record from the store. + * + * @param key The key of the record to get. + * @param options The options for getting the record. + * @return A Mono emitting the record. + */ + @Override + public Mono getAsync(String key, GetRecordOptions options) { + return getBatchAsync(Collections.singletonList(key), options) + .mapNotNull(records -> { + if (records.isEmpty()) { + return null; + } + + return records.get(0); + }); + } + + /** + * Gets a batch of records from the store. + * + * @param keys The keys of the records to get. + * @param options The options for getting the records. + * @return A Mono emitting a list of records. + */ + @Override + public Mono> getBatchAsync(List keys, + GetRecordOptions options) { + Pipeline pipeline = client.pipelined(); + List>> responses = new ArrayList<>(keys.size()); + keys.forEach(key -> { + String redisKey = prefixKeyIfNeeded(key, collectionName); + + if (options != null && options.isIncludeVectors()) { + // Returns Map with the fields and values + responses + .add(new AbstractMap.SimpleEntry<>(key, + pipeline.hgetAll(stringToBytes(redisKey)))); + } else { + // Returns List with the values of the fields + responses + .add(new AbstractMap.SimpleEntry<>(key, + pipeline.hmget(stringToBytes(redisKey), dataFields))); + } + }); + + return Mono.defer(() -> { + pipeline.sync(); + + try { + return Mono.just(responses.stream() + .map(entry -> { + if (options != null && options.isIncludeVectors()) { + // Results directly in a Map + return this.vectorStoreRecordMapper + .mapStorageModelToRecord( + new AbstractMap.SimpleEntry<>(entry.getKey(), + (Map) entry.getValue().get()), + options); + } + + // Results in a List with the values of the fields + return this.vectorStoreRecordMapper + .mapStorageModelToRecord( + new AbstractMap.SimpleEntry<>(entry.getKey(), + addDataFieldNames((List) entry.getValue().get())), + options); + }) + .collect(Collectors.toList())); + } catch (Exception e) { + return Mono.error(e); + } + }).subscribeOn(Schedulers.boundedElastic()); + } + + /** + * Inserts or updates a record in the store. + * + * @param data The record to upsert. + * @param options The options for upserting the record. + * @return A Mono emitting the key of the upserted record. + */ + @Override + public Mono upsertAsync(Record data, UpsertRecordOptions options) { + Map.Entry> redisObject = this.vectorStoreRecordMapper + .mapRecordToStorageModel(data); + String redisKey = prefixKeyIfNeeded(redisObject.getKey(), collectionName); + + return Mono.fromRunnable(() -> client.hset(stringToBytes(redisKey), redisObject.getValue())) + .subscribeOn(Schedulers.boundedElastic()) + .thenReturn(redisObject.getKey()); + } + + /** + * Inserts or updates a batch of records in the store. + * + * @param data The records to upsert. + * @param options The options for upserting the records. + * @return A Mono emitting a collection of keys of the upserted records. + */ + @Override + public Mono> upsertBatchAsync(List data, UpsertRecordOptions options) { + Pipeline pipeline = client.pipelined(); + List keys = new ArrayList<>(data.size()); + + data.forEach(record -> { + Map.Entry> redisObject = this.vectorStoreRecordMapper + .mapRecordToStorageModel(record); + String redisKey = prefixKeyIfNeeded(redisObject.getKey(), collectionName); + + keys.add(redisObject.getKey()); + pipeline.hset(stringToBytes(redisKey), redisObject.getValue()); + }); + + return Mono.fromRunnable(pipeline::sync) + .subscribeOn(Schedulers.boundedElastic()) + .thenReturn(keys); + } + + /** + * Deletes a record from the store. + * + * @param key The key of the record to delete. + * @param options The options for deleting the record. + * @return A Mono representing the completion of the deletion operation. + */ + @Override + public Mono deleteAsync(String key, DeleteRecordOptions options) { + String redisKey = prefixKeyIfNeeded(key, collectionName); + + return Mono.fromRunnable(() -> client.del(redisKey)) + .subscribeOn(Schedulers.boundedElastic()) + .then(); + } + + /** + * Deletes a batch of records from the store. + * + * @param strings The keys of the records to delete. + * @param options The options for deleting the records. + * @return A Mono representing the completion of the deletion operation. + */ + @Override + public Mono deleteBatchAsync(List strings, DeleteRecordOptions options) { + Pipeline pipeline = client.pipelined(); + strings.forEach(key -> { + String redisKey = prefixKeyIfNeeded(key, collectionName); + pipeline.del(redisKey); + }); + + return Mono.fromRunnable(pipeline::sync) + .subscribeOn(Schedulers.boundedElastic()) + .then(); + } + + /** + * Vectorized search. This method searches for records that are similar to the given vector. + * + * @param vector The vector to search with. + * @param options The options to use for the search. + * @return A list of search results. + */ + @Override + public Mono> searchAsync(List vector, + VectorSearchOptions options) { + if (recordDefinition.getVectorFields().isEmpty()) { + return Mono + .error(new SKException("No vector fields defined. Cannot perform vector search")); + } + + return createCollectionIfNotExistsAsync().flatMap(collection -> Mono.fromCallable(() -> { + Pair ftSearchParams = RedisVectorStoreCollectionSearchMapping + .getInstance() + .buildQuery(vector, options, recordDefinition, RedisStorageType.HASH_SET); + + SearchResult searchResult = client.ftSearch(collectionName, ftSearchParams.getLeft(), + ftSearchParams.getRight()); + + List> results = searchResult.getDocuments().stream() + .map(doc -> { + String key = removeKeyPrefixIfNeeded(doc.getId(), collectionName); + double score = 0; + + // Convert from Map to Map + Map storage = new HashMap<>(); + for (Map.Entry entry : doc.getProperties()) { + // Data and vector fields are returned as byte[] + if (entry.getValue() instanceof byte[]) { + storage.put(stringToBytes(entry.getKey()), + (byte[]) entry.getValue()); + } + // Score is returned as a string + else if (entry.getKey().equals( + RedisVectorStoreCollectionSearchMapping.VECTOR_SCORE_FIELD)) { + // Score is stored as a string in one of the fields + score = Double.parseDouble((String) entry.getValue()); + } + } + + Record record = this.vectorStoreRecordMapper + .mapStorageModelToRecord( + new AbstractMap.SimpleEntry<>(key, storage), + new GetRecordOptions( + options != null && options.isIncludeVectors())); + + return new VectorSearchResult<>(record, score); + }) + .collect(Collectors.toList()); + + return new VectorSearchResults<>(results); + }).subscribeOn(Schedulers.boundedElastic())); + } +} diff --git a/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordCollectionOptions.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordCollectionOptions.java new file mode 100644 index 000000000..c4730d2ad --- /dev/null +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordCollectionOptions.java @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Options for a Redis hash set vector store record collection. + * @param the record type + */ +public class RedisHashSetVectorStoreRecordCollectionOptions + implements VectorStoreRecordCollectionOptions { + private final Class recordClass; + @Nullable + private final VectorStoreRecordMapper>> vectorStoreRecordMapper; + @Nullable + private final VectorStoreRecordDefinition recordDefinition; + private final boolean prefixCollectionName; + @Nullable + private final ObjectMapper objectMapper; + + private RedisHashSetVectorStoreRecordCollectionOptions( + @Nonnull Class recordClass, + @Nullable VectorStoreRecordMapper>> vectorStoreRecordMapper, + @Nullable VectorStoreRecordDefinition recordDefinition, + boolean prefixCollectionName, + @Nullable ObjectMapper objectMapper) { + this.recordClass = recordClass; + this.vectorStoreRecordMapper = vectorStoreRecordMapper; + this.recordDefinition = recordDefinition; + this.prefixCollectionName = prefixCollectionName; + this.objectMapper = objectMapper; + } + + /** + * Creates a new builder. + * + * @param the record type + * @return the builder + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * Gets the key class. + * + * @return the key class + */ + @Override + public Class getKeyClass() { + return String.class; + } + + /** + * Gets the record class. + * + * @return the record class + */ + public Class getRecordClass() { + return recordClass; + } + + /** + * Gets the record definition. + * + * @return the record definition + */ + @Nullable + public VectorStoreRecordDefinition getRecordDefinition() { + return recordDefinition; + } + + /** + * Gets the vector store record mapper. + * + * @return the vector store record mapper + */ + @Nullable + public VectorStoreRecordMapper>> getVectorStoreRecordMapper() { + return vectorStoreRecordMapper; + } + + /** + * Gets whether to prefix the collection name to the redis key. + * + * @return whether to prefix the collection name to the redis key + */ + public boolean isPrefixCollectionName() { + return prefixCollectionName; + } + + /** + * Gets the object mapper. + * + * @return the object mapper + */ + @Nullable + ObjectMapper getObjectMapper() { + return objectMapper; + } + + /** + * Builder for {@link RedisHashSetVectorStoreRecordCollectionOptions}. + * + * @param the record type + */ + public static class Builder { + @Nullable + private VectorStoreRecordMapper>> vectorStoreRecordMapper; + @Nullable + private Class recordClass; + @Nullable + private VectorStoreRecordDefinition recordDefinition; + private boolean prefixCollectionName = true; + @Nullable + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Sets the record class. + * + * @param recordClass the record class + * @return the builder + */ + public Builder withRecordClass(Class recordClass) { + this.recordClass = recordClass; + return this; + } + + /** + * Sets the vector store record mapper. + * + * @param vectorStoreRecordMapper the vector store record mapper + * @return the builder + */ + public Builder withVectorStoreRecordMapper( + VectorStoreRecordMapper>> vectorStoreRecordMapper) { + this.vectorStoreRecordMapper = vectorStoreRecordMapper; + return this; + } + + /** + * Sets the record definition. + * + * @param recordDefinition the record definition + * @return the builder + */ + public Builder withRecordDefinition(VectorStoreRecordDefinition recordDefinition) { + this.recordDefinition = recordDefinition; + return this; + } + + /** + * Sets whether to prefix the collection name to the redis key. + * Default is true. + * + * @param prefixCollectionName whether to prefix the collection name to the redis key + * @return the builder + */ + public Builder withPrefixCollectionName(boolean prefixCollectionName) { + this.prefixCollectionName = prefixCollectionName; + return this; + } + + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Builds the options. + * + * @return the options + */ + public RedisHashSetVectorStoreRecordCollectionOptions build() { + if (recordClass == null) { + throw new SKException("recordClass must be provided"); + } + + return new RedisHashSetVectorStoreRecordCollectionOptions<>( + recordClass, + vectorStoreRecordMapper, + recordDefinition, + prefixCollectionName, + objectMapper); + } + } +} diff --git a/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordMapper.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordMapper.java new file mode 100644 index 000000000..5ba97dfea --- /dev/null +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisHashSetVectorStoreRecordMapper.java @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.redis; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static com.microsoft.semantickernel.data.redis.RedisHashSetVectorStoreRecordCollection.stringToBytes; + +/** + * A mapper to convert between a record and a Redis hash set storage model. + * @param the record type + */ +public class RedisHashSetVectorStoreRecordMapper + extends VectorStoreRecordMapper>> { + + private RedisHashSetVectorStoreRecordMapper( + Function>> toStorageModelMapper, + BiFunction>, GetRecordOptions, Record> toRecordMapper) { + super(toStorageModelMapper, toRecordMapper); + } + + /** + * Creates a new builder. + * + * @param the record type + * @return the builder + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * Creates a new builder. + * + * @param the record type + */ + public static class Builder + implements SemanticKernelBuilder> { + @Nullable + private Class recordClass; + @Nullable + private VectorStoreRecordDefinition recordDefinition; + @Nullable + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Sets the record class. + * + * @param recordClass the record class + * @return the builder + */ + public Builder withRecordClass(Class recordClass) { + this.recordClass = recordClass; + return this; + } + + /** + * Sets the vector store record definition. + * + * @param recordDefinition the vector store record definition + * @return the builder + */ + public Builder withVectorStoreRecordDefinition( + VectorStoreRecordDefinition recordDefinition) { + this.recordDefinition = recordDefinition; + return this; + } + + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Builds the {@link RedisHashSetVectorStoreRecordMapper}. + * + * @return the {@link RedisHashSetVectorStoreRecordMapper} + */ + @Override + public RedisHashSetVectorStoreRecordMapper build() { + if (recordClass == null) { + throw new SKException("recordClass is required"); + } + if (recordDefinition == null) { + throw new SKException("vectorStoreRecordDefinition is required"); + } + + return new RedisHashSetVectorStoreRecordMapper<>(record -> { + try { + ObjectNode jsonNode = objectMapper.valueToTree(record); + + String key = jsonNode + .get(recordDefinition.getKeyField().getEffectiveStorageName()).asText(); + jsonNode.remove(recordDefinition.getKeyField().getEffectiveStorageName()); + + Map storage = new HashMap<>(); + for (VectorStoreRecordDataField field : recordDefinition.getDataFields()) { + JsonNode value = jsonNode.get(field.getEffectiveStorageName()); + if (value != null) { + storage.put( + stringToBytes(field.getEffectiveStorageName()), + objectMapper.writeValueAsBytes(value)); + } + } + for (VectorStoreRecordVectorField field : recordDefinition.getVectorFields()) { + ArrayNode value = (ArrayNode) jsonNode.get(field.getEffectiveStorageName()); + List vector = objectMapper.convertValue(value, List.class); + if (value != null) { + storage.put( + stringToBytes(field.getEffectiveStorageName()), + RedisVectorStoreCollectionSearchMapping + .convertListToByteArray(vector)); + } + } + + return new AbstractMap.SimpleEntry<>(key, storage); + } catch (Exception e) { + throw new SKException( + "Failure to serialize object, by default the Redis connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", + e); + } + }, (storageModel, options) -> { + try { + // Empty map means no record found + if (storageModel.getValue() == null || storageModel.getValue().isEmpty()) { + return null; + } + + ObjectNode jsonNode = objectMapper.createObjectNode(); + jsonNode.set(recordDefinition.getKeyField().getEffectiveStorageName(), + objectMapper.valueToTree(storageModel.getKey())); + + // byte[] as key is not useful, convert to String + Map storage = new HashMap<>(); + storageModel.getValue() + .forEach((k, v) -> storage.put(new String(k, StandardCharsets.UTF_8), v)); + + for (VectorStoreRecordDataField field : recordDefinition.getDataFields()) { + byte[] value = storage.get(field.getEffectiveStorageName()); + if (value != null) { + jsonNode.set(field.getEffectiveStorageName(), + objectMapper.valueToTree( + objectMapper.readValue(value, field.getFieldType()))); + } + } + if (options != null && options.isIncludeVectors()) { + for (VectorStoreRecordVectorField field : recordDefinition + .getVectorFields()) { + byte[] value = storage.get(field.getEffectiveStorageName()); + if (value != null) { + jsonNode.set(field.getEffectiveStorageName(), + objectMapper.valueToTree(RedisVectorStoreCollectionSearchMapping + .convertByteArrayToList(value))); + } + } + } + + return objectMapper.convertValue(jsonNode, recordClass); + } catch (JsonProcessingException e) { + throw new SKException( + "Failure to deserialize object, by default the Redis connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", + e); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollection.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordCollection.java similarity index 62% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollection.java rename to data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordCollection.java index 52d30bf84..50db6bd9b 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollection.java +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordCollection.java @@ -1,17 +1,37 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.redis; +package com.microsoft.semantickernel.data.redis; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.microsoft.semantickernel.data.VectorStoreRecordCollection; -import com.microsoft.semantickernel.data.VectorStoreRecordMapper; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDataField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.data.recordoptions.DeleteRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorsearch.VectorizedSearch; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.options.DeleteRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +import org.apache.commons.lang3.tuple.Pair; import org.json.JSONArray; import org.json.JSONObject; import reactor.core.publisher.Mono; @@ -21,24 +41,20 @@ import redis.clients.jedis.Response; import redis.clients.jedis.exceptions.JedisDataException; import redis.clients.jedis.json.Path2; +import redis.clients.jedis.search.FTSearchParams; import redis.clients.jedis.search.IndexDefinition; import redis.clients.jedis.search.IndexOptions; import redis.clients.jedis.search.Schema; +import redis.clients.jedis.search.SearchResult; -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.AbstractMap.SimpleEntry; -import java.util.stream.Collectors; - -public class RedisVectorStoreRecordCollection - implements VectorStoreRecordCollection { +/** + * Represents a Redis vector store record collection. + * + * @param The type of record in the collection. + */ +public class RedisJsonVectorStoreRecordCollection + implements VectorStoreRecordCollection, + VectorizedSearch { private static final HashSet> supportedKeyTypes = new HashSet<>( Collections.singletonList( @@ -51,23 +67,24 @@ public class RedisVectorStoreRecordCollection private final JedisPooled client; private final String collectionName; - private final RedisVectorStoreRecordCollectionOptions options; + private final RedisJsonVectorStoreRecordCollectionOptions options; private final VectorStoreRecordMapper> vectorStoreRecordMapper; private final VectorStoreRecordDefinition recordDefinition; private final Path2[] dataFields; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; /** * Creates a new instance of the RedisVectorRecordStore. * * @param client The Redis client. + * @param collectionName The name of the collection. * @param options The options for the store. */ @SuppressFBWarnings("EI_EXPOSE_REP2") - public RedisVectorStoreRecordCollection( + public RedisJsonVectorStoreRecordCollection( @Nonnull JedisPooled client, @Nonnull String collectionName, - @Nonnull RedisVectorStoreRecordCollectionOptions options) { + @Nonnull RedisJsonVectorStoreRecordCollectionOptions options) { this.client = client; this.collectionName = collectionName; this.options = options; @@ -82,18 +99,22 @@ public RedisVectorStoreRecordCollection( // Validate supported types VectorStoreRecordDefinition.validateSupportedTypes( - Collections - .singletonList(recordDefinition.getKeyDeclaredField(this.options.getRecordClass())), + Collections.singletonList(recordDefinition.getKeyField()), supportedKeyTypes); VectorStoreRecordDefinition.validateSupportedTypes( - recordDefinition.getVectorDeclaredFields(this.options.getRecordClass()), + new ArrayList<>(recordDefinition.getVectorFields()), supportedVectorTypes); + // If object mapper is not provided, set a default one + this.objectMapper = options.getObjectMapper() != null ? options.getObjectMapper() + : new ObjectMapper(); + // If mapper is not provided, set a default one if (options.getVectorStoreRecordMapper() == null) { - vectorStoreRecordMapper = new RedisVectorStoreRecordMapper.Builder() - .withKeyFieldName(recordDefinition.getKeyField().getName()) + vectorStoreRecordMapper = new RedisJsonVectorStoreRecordMapper.Builder() .withRecordClass(options.getRecordClass()) + .withRecordDefinition(recordDefinition) + .withObjectMapper(objectMapper) .build(); } else { vectorStoreRecordMapper = options.getVectorStoreRecordMapper(); @@ -102,7 +123,7 @@ public RedisVectorStoreRecordCollection( // Creates a list of paths to retrieve from Redis when no vectors are requested // Paths are in the format of $.field this.dataFields = recordDefinition.getDataFields().stream() - .map(VectorStoreRecordDataField::getName) + .map(VectorStoreRecordDataField::getEffectiveStorageName) .map(Path2::new) .toArray(Path2[]::new); } @@ -143,10 +164,10 @@ public Mono collectionExistsAsync() { * @return A Mono representing the completion of the creation operation. */ @Override - public Mono createCollectionAsync() { + public Mono> createCollectionAsync() { return Mono.fromRunnable(() -> { Schema schema = RedisVectorStoreCollectionCreateMapping - .mapToSchema(recordDefinition.getAllFields()); + .mapToSchema(recordDefinition.getAllFields(), RedisStorageType.JSON); IndexDefinition indexDefinition = new IndexDefinition(IndexDefinition.Type.JSON) .setPrefixes(collectionName + ":"); @@ -155,17 +176,19 @@ public Mono createCollectionAsync() { collectionName, IndexOptions.defaultOptions().setDefinition(indexDefinition), schema); - }).subscribeOn(Schedulers.boundedElastic()).then(); + }) + .subscribeOn(Schedulers.boundedElastic()) + .then(Mono.just(this)); } @Override - public Mono createCollectionIfNotExistsAsync() { + public Mono> createCollectionIfNotExistsAsync() { return collectionExistsAsync().flatMap(exists -> { if (!exists) { return createCollectionAsync(); } - return Mono.empty(); + return Mono.just(this); }); } @@ -181,10 +204,17 @@ public Mono deleteCollectionAsync() { .then(); } - private String getRedisKey(String key, String collectionName) { + private String prefixKeyIfNeeded(String key, String collectionName) { return options.isPrefixCollectionName() ? collectionName + ":" + key : key; } + private String removeKeyPrefixIfNeeded(String key, String collectionName) { + if (options.isPrefixCollectionName() && key.startsWith(collectionName + ":")) { + return key.substring(collectionName.length() + 1); + } + return key; + } + private JsonNode removeRedisPathPrefix(JSONObject object) { ObjectNode noPathPrefix = objectMapper.createObjectNode(); object.keySet().forEach(key -> { @@ -203,18 +233,18 @@ private JsonNode removeRedisPathPrefix(JSONObject object) { /** * Gets a record from the store. * - * @param key The key of the record to get. + * @param key The key of the record to get. * @param options The options for getting the record. * @return A Mono emitting the record. */ @Override public Mono getAsync(String key, GetRecordOptions options) { - String redisKey = getRedisKey(key, collectionName); + String redisKey = prefixKeyIfNeeded(key, collectionName); return Mono.defer(() -> { try { Object value; - if (options == null || options.includeVectors()) { + if (options != null && options.isIncludeVectors()) { value = client.jsonGet(redisKey); } else { value = client.jsonGet(redisKey, dataFields); @@ -225,7 +255,7 @@ public Mono getAsync(String key, GetRecordOptions options) { } JsonNode jsonNode; - if (options == null || options.includeVectors()) { + if (options != null && options.isIncludeVectors()) { jsonNode = objectMapper.valueToTree(value); } else { // Remove the $. prefix from every key in the JSON object @@ -233,7 +263,7 @@ public Mono getAsync(String key, GetRecordOptions options) { } return Mono.just(this.vectorStoreRecordMapper - .mapStorageModeltoRecord(new SimpleEntry<>(key, jsonNode))); + .mapStorageModelToRecord(new SimpleEntry<>(key, jsonNode), options)); } catch (Exception e) { return Mono.error(e); } @@ -243,7 +273,7 @@ public Mono getAsync(String key, GetRecordOptions options) { /** * Gets a batch of records from the store. * - * @param keys The keys of the records to get. + * @param keys The keys of the records to get. * @param options The options for getting the records. * @return A Mono emitting a list of records. */ @@ -253,9 +283,9 @@ public Mono> getBatchAsync(List keys, Pipeline pipeline = client.pipelined(); List>> responses = new ArrayList<>(keys.size()); keys.forEach(key -> { - String redisKey = getRedisKey(key, collectionName); + String redisKey = prefixKeyIfNeeded(key, collectionName); - if (options == null || options.includeVectors()) { + if (options != null && options.isIncludeVectors()) { responses.add(new SimpleEntry<>(key, pipeline.jsonGet(redisKey))); } else { responses.add(new SimpleEntry<>(key, pipeline.jsonGet(redisKey, dataFields))); @@ -274,13 +304,14 @@ public Mono> getBatchAsync(List keys, } JsonNode jsonNode; - if (options == null || options.includeVectors()) { + if (options != null && options.isIncludeVectors()) { jsonNode = objectMapper.valueToTree(value); } else { jsonNode = removeRedisPathPrefix((JSONObject) value); } return this.vectorStoreRecordMapper - .mapStorageModeltoRecord(new SimpleEntry<>(entry.getKey(), jsonNode)); + .mapStorageModelToRecord(new SimpleEntry<>(entry.getKey(), jsonNode), + options); }) .collect(Collectors.toList())); } catch (Exception e) { @@ -300,7 +331,7 @@ public Mono> getBatchAsync(List keys, public Mono upsertAsync(Record data, UpsertRecordOptions options) { Entry redisObject = this.vectorStoreRecordMapper .mapRecordToStorageModel(data); - String redisKey = getRedisKey(redisObject.getKey(), collectionName); + String redisKey = prefixKeyIfNeeded(redisObject.getKey(), collectionName); return Mono.fromRunnable(() -> client.jsonSet(redisKey, redisObject.getValue())) .subscribeOn(Schedulers.boundedElastic()) @@ -322,7 +353,7 @@ public Mono> upsertBatchAsync(List data, UpsertRecordOption data.forEach(record -> { Entry redisObject = this.vectorStoreRecordMapper .mapRecordToStorageModel(record); - String redisKey = getRedisKey(redisObject.getKey(), collectionName); + String redisKey = prefixKeyIfNeeded(redisObject.getKey(), collectionName); keys.add(redisObject.getKey()); pipeline.jsonSet(redisKey, redisObject.getValue()); @@ -336,13 +367,13 @@ public Mono> upsertBatchAsync(List data, UpsertRecordOption /** * Deletes a record from the store. * - * @param key The key of the record to delete. + * @param key The key of the record to delete. * @param options The options for deleting the record. * @return A Mono representing the completion of the deletion operation. */ @Override public Mono deleteAsync(String key, DeleteRecordOptions options) { - String redisKey = getRedisKey(key, collectionName); + String redisKey = prefixKeyIfNeeded(key, collectionName); return Mono.fromRunnable(() -> client.del(redisKey)) .subscribeOn(Schedulers.boundedElastic()) @@ -360,7 +391,7 @@ public Mono deleteAsync(String key, DeleteRecordOptions options) { public Mono deleteBatchAsync(List strings, DeleteRecordOptions options) { Pipeline pipeline = client.pipelined(); strings.forEach(key -> { - String redisKey = getRedisKey(key, collectionName); + String redisKey = prefixKeyIfNeeded(key, collectionName); pipeline.del(redisKey); }); @@ -368,4 +399,55 @@ public Mono deleteBatchAsync(List strings, DeleteRecordOptions opt .subscribeOn(Schedulers.boundedElastic()) .then(); } + + /** + * Vectorized search. This method searches for records that are similar to the given vector. + * + * @param vector The vector to search with. + * @param options The options to use for the search. + * @return The search results. + */ + @Override + public Mono> searchAsync(List vector, + VectorSearchOptions options) { + if (recordDefinition.getVectorFields().isEmpty()) { + return Mono + .error(new SKException("No vector fields defined. Cannot perform vector search")); + } + + return createCollectionIfNotExistsAsync().flatMap(collection -> Mono.fromCallable(() -> { + Pair ftSearchParams = RedisVectorStoreCollectionSearchMapping + .getInstance().buildQuery(vector, options, recordDefinition, RedisStorageType.JSON); + + SearchResult searchResult = client.ftSearch(collectionName, + ftSearchParams.getLeft(), ftSearchParams.getRight()); + + List> results = searchResult.getDocuments().stream() + .map(doc -> { + Map properties = new HashMap<>(); + for (Map.Entry entry : doc.getProperties()) { + properties.put(entry.getKey(), entry.getValue()); + } + + String key = removeKeyPrefixIfNeeded(doc.getId(), collectionName); + String value = (String) properties.get("$"); + double score = Double.parseDouble((String) properties + .get(RedisVectorStoreCollectionSearchMapping.VECTOR_SCORE_FIELD)); + + try { + JsonNode jsonNode = objectMapper.readTree(value); + Record record = this.vectorStoreRecordMapper + .mapStorageModelToRecord(new SimpleEntry<>(key, jsonNode), + new GetRecordOptions( + options != null && options.isIncludeVectors())); + + return new VectorSearchResult<>(record, score); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toList()); + + return new VectorSearchResults<>(results); + }).subscribeOn(Schedulers.boundedElastic())); + } } diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollectionOptions.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordCollectionOptions.java similarity index 60% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollectionOptions.java rename to data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordCollectionOptions.java index 93c307077..0ba8b9018 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollectionOptions.java +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordCollectionOptions.java @@ -1,30 +1,43 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.redis; +package com.microsoft.semantickernel.data.redis; -import com.microsoft.semantickernel.data.VectorStoreRecordMapper; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Map.Entry; -public class RedisVectorStoreRecordCollectionOptions { +/** + * Options for a Redis vector store record collection. + * + * @param the record type + */ +public class RedisJsonVectorStoreRecordCollectionOptions + implements VectorStoreRecordCollectionOptions { private final Class recordClass; @Nullable private final VectorStoreRecordMapper> vectorStoreRecordMapper; @Nullable private final VectorStoreRecordDefinition recordDefinition; private final boolean prefixCollectionName; + private final ObjectMapper objectMapper; - private RedisVectorStoreRecordCollectionOptions( + private RedisJsonVectorStoreRecordCollectionOptions( @Nonnull Class recordClass, @Nullable VectorStoreRecordMapper> vectorStoreRecordMapper, @Nullable VectorStoreRecordDefinition recordDefinition, - boolean prefixCollectionName) { + boolean prefixCollectionName, + @Nullable ObjectMapper objectMapper) { this.recordClass = recordClass; this.vectorStoreRecordMapper = vectorStoreRecordMapper; this.recordDefinition = recordDefinition; this.prefixCollectionName = prefixCollectionName; + this.objectMapper = objectMapper; } /** @@ -37,6 +50,16 @@ public static Builder builder() { return new Builder<>(); } + /** + * Gets the key class. + * + * @return the key class + */ + @Override + public Class getKeyClass() { + return String.class; + } + /** * Gets the record class. * @@ -76,7 +99,16 @@ public boolean isPrefixCollectionName() { } /** - * Builder for {@link RedisVectorStoreRecordCollectionOptions}. + * Gets the object mapper. + * + * @return the object mapper + */ + ObjectMapper getObjectMapper() { + return objectMapper; + } + + /** + * Builder for {@link RedisJsonVectorStoreRecordCollectionOptions}. * * @param the record type */ @@ -88,6 +120,8 @@ public static class Builder { @Nullable private VectorStoreRecordDefinition recordDefinition; private boolean prefixCollectionName = true; + @Nullable + private ObjectMapper objectMapper = new ObjectMapper(); /** * Sets the record class. @@ -124,10 +158,11 @@ public Builder withRecordDefinition(VectorStoreRecordDefinition recordDe } /** - * Sets whether to prefix the collection name to the redis key. - * Default is true. - * - * @param prefixCollectionName whether to prefix the collection name to the redis key + * Sets whether the collection name should be prefixed to the key names before reading or writing to the Redis store. Default is true. + *

+ * For a record to be indexed by a specific Redis index, the key name must be prefixed with the matching prefix configured on the Redis index. + * You can either pass in keys that are already prefixed, or set this option to true to have the collection name prefixed to the key names automatically. + * @param prefixCollectionName whether to prefix the collection name to the key * @return the builder */ public Builder withPrefixCollectionName(boolean prefixCollectionName) { @@ -135,21 +170,34 @@ public Builder withPrefixCollectionName(boolean prefixCollectionName) { return this; } + /** + * Sets the object mapper to use for serialization and deserialization. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + /** * Builds the options. * * @return the options */ - public RedisVectorStoreRecordCollectionOptions build() { + public RedisJsonVectorStoreRecordCollectionOptions build() { if (recordClass == null) { - throw new IllegalArgumentException("recordClass must be provided"); + throw new SKException("recordClass must be provided"); } - return new RedisVectorStoreRecordCollectionOptions<>( + return new RedisJsonVectorStoreRecordCollectionOptions<>( recordClass, vectorStoreRecordMapper, recordDefinition, - prefixCollectionName); + prefixCollectionName, + objectMapper); } } } diff --git a/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordMapper.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordMapper.java new file mode 100644 index 000000000..f0de10bb2 --- /dev/null +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisJsonVectorStoreRecordMapper.java @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordMapper; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import java.util.AbstractMap; +import java.util.Map.Entry; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * A mapper to convert between a record and a Redis JSON storage model. + * @param the record type + */ +public class RedisJsonVectorStoreRecordMapper + extends VectorStoreRecordMapper> { + + private RedisJsonVectorStoreRecordMapper( + Function> toStorageModelMapper, + BiFunction, GetRecordOptions, Record> toRecordMapper) { + super(toStorageModelMapper, toRecordMapper); + } + + /** + * Creates a new builder. + * + * @param the record type + * @return the builder + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * Creates a new builder. + * + * @param the record type + */ + public static class Builder + implements SemanticKernelBuilder> { + @Nullable + private Class recordClass; + @Nullable + private VectorStoreRecordDefinition recordDefinition; + + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Sets the record class. + * + * @param recordClass the record class + * @return the builder + */ + public Builder withRecordClass(Class recordClass) { + this.recordClass = recordClass; + return this; + } + + /** + * Sets the record definition. + * + * @param recordDefinition the record definition + * @return the builder + */ + public Builder withRecordDefinition(VectorStoreRecordDefinition recordDefinition) { + this.recordDefinition = recordDefinition; + return this; + } + + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Builds the {@link RedisJsonVectorStoreRecordMapper}. + * + * @return the {@link RedisJsonVectorStoreRecordMapper} + */ + @Override + public RedisJsonVectorStoreRecordMapper build() { + if (recordClass == null) { + throw new SKException("recordClass is required"); + } + if (recordDefinition == null) { + throw new SKException("recordDefinition is required"); + } + + return new RedisJsonVectorStoreRecordMapper<>(record -> { + try { + String keyFieldName = recordDefinition.getKeyField().getEffectiveStorageName(); + ObjectNode jsonNode = objectMapper.valueToTree(record); + String key = jsonNode.get(keyFieldName).asText(); + jsonNode.remove(keyFieldName); + + return new AbstractMap.SimpleEntry<>(key, jsonNode); + } catch (Exception e) { + throw new SKException( + "Failure to serialize object, by default the Redis connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", + e); + } + }, (storageModel, options) -> { + try { + String keyFieldName = recordDefinition.getKeyField().getEffectiveStorageName(); + ObjectNode jsonNode = objectMapper.valueToTree(storageModel.getValue()); + // Add the key back to the record + jsonNode.put(keyFieldName, storageModel.getKey()); + // Make sure to exclude the vectors if needed + if (options == null || !options.isIncludeVectors()) { + for (VectorStoreRecordVectorField vectorField : recordDefinition + .getVectorFields()) { + if (jsonNode.has(vectorField.getEffectiveStorageName())) { + jsonNode.remove(vectorField.getEffectiveStorageName()); + } + } + } + return objectMapper.convertValue(jsonNode, recordClass); + } catch (Exception e) { + throw new SKException( + "Failure to deserialize object, by default the Redis connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", + e); + } + }); + } + } +} diff --git a/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisStorageType.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisStorageType.java new file mode 100644 index 000000000..066e65249 --- /dev/null +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisStorageType.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.redis; + +/** + * The storage type for the Redis vector store. + */ +public enum RedisStorageType { + /** + * Redis storage with JSON module. + */ + JSON, + /** + * Redis storage with hash set. + */ + HASH_SET +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStore.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStore.java similarity index 55% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStore.java rename to data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStore.java index 7e561e437..6a0f1cee8 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStore.java +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStore.java @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.redis; +package com.microsoft.semantickernel.data.redis; import com.microsoft.semantickernel.builders.SemanticKernelBuilder; -import com.microsoft.semantickernel.data.VectorStore; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.VectorStore; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.exceptions.SKException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; import java.util.List; @@ -12,7 +14,10 @@ import reactor.core.publisher.Mono; import redis.clients.jedis.JedisPooled; -public class RedisVectorStore implements VectorStore> { +/** + * Represents a Redis vector store. + */ +public class RedisVectorStore implements VectorStore { private final JedisPooled client; private final RedisVectorStoreOptions options; @@ -34,32 +39,42 @@ public RedisVectorStore(@Nonnull JedisPooled client, * Gets a collection from the vector store. * * @param collectionName The name of the collection. - * @param recordClass The class type of the record. - * @param recordDefinition The record definition. + * @param options The options for the collection. + * @param The type of key in the collection. + * @param The type of record in the collection. * @return The collection. */ - @Override - public RedisVectorStoreRecordCollection getCollection( + public VectorStoreRecordCollection getCollection( @Nonnull String collectionName, - @Nonnull Class recordClass, - @Nullable VectorStoreRecordDefinition recordDefinition) { + @Nonnull VectorStoreRecordCollectionOptions options) { + if (!options.getKeyClass().equals(String.class)) { + throw new SKException("Redis only supports string keys"); + } + if (options.getRecordClass() == null) { + throw new SKException("Record class is required"); + } - if (options.getVectorStoreRecordCollectionFactory() != null) { - return options.getVectorStoreRecordCollectionFactory() + if (this.options.getVectorStoreRecordCollectionFactory() != null) { + return (VectorStoreRecordCollection) this.options + .getVectorStoreRecordCollectionFactory() .createVectorStoreRecordCollection( client, collectionName, - RedisVectorStoreRecordCollectionOptions.builder() - .withRecordClass(recordClass) - .withRecordDefinition(recordDefinition) - .build()); + options.getRecordClass(), + options.getRecordDefinition()); } - return new RedisVectorStoreRecordCollection<>(client, collectionName, - RedisVectorStoreRecordCollectionOptions.builder() - .withRecordClass(recordClass) - .withRecordDefinition(recordDefinition) - .build()); + if (this.options.getStorageType() == RedisStorageType.JSON) { + return (VectorStoreRecordCollection) new RedisJsonVectorStoreRecordCollection<>( + client, + collectionName, + (RedisJsonVectorStoreRecordCollectionOptions) options); + } else { + return (VectorStoreRecordCollection) new RedisHashSetVectorStoreRecordCollection<>( + client, + collectionName, + (RedisHashSetVectorStoreRecordCollectionOptions) options); + } } /** @@ -73,13 +88,16 @@ public Mono> getCollectionNamesAsync() { } /** - * Builder for the Redis vector store. - * + * Create a builder for the Redis vector store. + * @return A new builder */ public static Builder builder() { return new Builder(); } + /** + * Builder for the Redis vector store. + */ public static class Builder implements SemanticKernelBuilder { @Nullable @@ -113,11 +131,11 @@ public Builder withOptions(RedisVectorStoreOptions options) { @Override public RedisVectorStore build() { if (client == null) { - throw new IllegalArgumentException("client is required"); + throw new SKException("client is required"); } if (options == null) { - throw new IllegalArgumentException("options is required"); + throw new SKException("options is required"); } return new RedisVectorStore(client, options); diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreCollectionCreateMapping.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreCollectionCreateMapping.java similarity index 60% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreCollectionCreateMapping.java rename to data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreCollectionCreateMapping.java index fe044c731..05222e258 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreCollectionCreateMapping.java +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreCollectionCreateMapping.java @@ -1,17 +1,24 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.redis; - -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDataField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordKeyField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordVectorField; +package com.microsoft.semantickernel.data.redis; + +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordKeyField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; + +import com.microsoft.semantickernel.exceptions.SKException; import redis.clients.jedis.search.Schema; +/** + * Maps a vector store record collection to a Redis schema. + */ public class RedisVectorStoreCollectionCreateMapping { private static final HashSet> supportedFilterableNumericTypes = new HashSet<>( Arrays.asList( @@ -30,26 +37,26 @@ public class RedisVectorStoreCollectionCreateMapping { private static String getAlgorithmMetric( VectorStoreRecordVectorField vectorField) { - if (vectorField.getDistanceFunction() == null) { + if (vectorField.getDistanceFunction() == DistanceFunction.UNDEFINED) { return RedisVectorDistanceMetric.COSINE; } switch (vectorField.getDistanceFunction()) { - case COSINE_SIMILARITY: + case COSINE_DISTANCE: return RedisVectorDistanceMetric.COSINE; case DOT_PRODUCT: return RedisVectorDistanceMetric.DOT_PRODUCT; - case EUCLIDEAN: + case EUCLIDEAN_DISTANCE: return RedisVectorDistanceMetric.EUCLIDEAN; default: - throw new IllegalArgumentException( + throw new SKException( "Unsupported distance function: " + vectorField.getDistanceFunction()); } } private static Schema.VectorField.VectorAlgo getAlgorithmConfig( VectorStoreRecordVectorField vectorField) { - if (vectorField.getIndexKind() == null) { + if (vectorField.getIndexKind() == IndexKind.UNDEFINED) { return Schema.VectorField.VectorAlgo.HNSW; } @@ -59,17 +66,26 @@ private static Schema.VectorField.VectorAlgo getAlgorithmConfig( case FLAT: return Schema.VectorField.VectorAlgo.FLAT; default: - throw new IllegalArgumentException( + throw new SKException( "Unsupported index kind: " + vectorField.getIndexKind()); } } - private static String getRedisPath(String name) { - return "$." + name; + private static String getRedisPath(String name, boolean withRedisJsonRoot) { + return withRedisJsonRoot ? "$." + name : name; } - public static Schema mapToSchema(List fields) { + /** + * Maps a vector store record collection to a Redis schema. + * + * @param fields the fields + * @param storageType the Redis storage type + * @return the schema + */ + public static Schema mapToSchema(List fields, + RedisStorageType storageType) { Schema schema = new Schema(); + boolean withRedisJsonRoot = storageType == RedisStorageType.JSON; for (VectorStoreRecordField field : fields) { if (field instanceof VectorStoreRecordKeyField) { @@ -81,18 +97,24 @@ public static Schema mapToSchema(List fields) { VectorStoreRecordDataField dataField = (VectorStoreRecordDataField) field; if (dataField.getFieldType() == null) { - throw new IllegalArgumentException( - "Field type is required for filterable fields: " + dataField.getName()); + throw new SKException( + "Field type is required for filterable fields: " + + dataField.getEffectiveStorageName()); } if (dataField.getFieldType().equals(String.class)) { - schema.addTextField(getRedisPath(dataField.getName()), 1.0); + schema.addTextField( + getRedisPath(dataField.getEffectiveStorageName(), withRedisJsonRoot), 1.0) + .as(dataField.getEffectiveStorageName()); } else if (supportedFilterableNumericTypes.contains(dataField.getFieldType())) { - schema.addNumericField(getRedisPath(dataField.getName())); + schema + .addNumericField( + getRedisPath(dataField.getEffectiveStorageName(), withRedisJsonRoot)) + .as(dataField.getEffectiveStorageName()); } else { - throw new IllegalArgumentException( + throw new SKException( "Unsupported field type for numeric filterable fields: " - + dataField.getName()); + + dataField.getEffectiveStorageName()); } } @@ -101,9 +123,9 @@ public static Schema mapToSchema(List fields) { VectorStoreRecordVectorField vectorField = (VectorStoreRecordVectorField) field; if (vectorField.getDimensions() < 1) { - throw new IllegalArgumentException( + throw new SKException( "Dimensions must be greater than 0 for vector fields: " - + vectorField.getName()); + + vectorField.getEffectiveStorageName()); } Schema.VectorField.VectorAlgo algorithm = getAlgorithmConfig(vectorField); @@ -114,7 +136,9 @@ public static Schema mapToSchema(List fields) { attributes.put(RedisIndexSchemaParams.DIMENSIONS, vectorField.getDimensions()); attributes.put(RedisIndexSchemaParams.DISTANCE_METRIC, metric); - schema.addVectorField(getRedisPath(vectorField.getName()), algorithm, attributes); + schema.addVectorField( + getRedisPath(vectorField.getEffectiveStorageName(), withRedisJsonRoot), + algorithm, attributes).as(vectorField.getEffectiveStorageName()); } } @@ -130,7 +154,7 @@ static class RedisIndexSchemaParams { static class RedisVectorDistanceMetric { public static final String EUCLIDEAN = "L2"; public static final String DOT_PRODUCT = "IP"; - public static final String COSINE = "COSINE"; + public static final String COSINE = "COSINE"; // Cosine distance } } diff --git a/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreCollectionSearchMapping.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreCollectionSearchMapping.java new file mode 100644 index 000000000..7f7d31bcf --- /dev/null +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreCollectionSearchMapping.java @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.redis; + +import com.microsoft.semantickernel.data.filter.AnyTagEqualToFilterClause; +import com.microsoft.semantickernel.data.filter.EqualToFilterClause; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.filter.FilterMapping; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; +import redis.clients.jedis.args.SortingOrder; +import redis.clients.jedis.search.FTSearchParams; + +/** + * A mapping for searching a collection of vector records in Redis. + */ +public class RedisVectorStoreCollectionSearchMapping implements FilterMapping { + + static final String VECTOR_SCORE_FIELD = "vector_score"; + + private RedisVectorStoreCollectionSearchMapping() { + } + + static class RedisVectorStoreCollectionSearchMappingHolder { + static final RedisVectorStoreCollectionSearchMapping INSTANCE = new RedisVectorStoreCollectionSearchMapping(); + } + + static RedisVectorStoreCollectionSearchMapping getInstance() { + return RedisVectorStoreCollectionSearchMappingHolder.INSTANCE; + } + + /** + * Builds a query for searching a collection of vector records in Redis. + * @param vector the vector to search for + * @param options the search options + * @param recordDefinition the record definition + * @param storageType the storage type + * @return the query and search parameters + */ + public Pair buildQuery(List vector, + VectorSearchOptions options, + VectorStoreRecordDefinition recordDefinition, + RedisStorageType storageType) { + VectorStoreRecordVectorField firstVectorField = recordDefinition.getVectorFields().get(0); + if (options == null) { + options = VectorSearchOptions.createDefault(firstVectorField.getName()); + } + + VectorStoreRecordVectorField vectorField = options.getVectorFieldName() == null + ? firstVectorField + : (VectorStoreRecordVectorField) recordDefinition + .getField(options.getVectorFieldName()); + + String filter = getFilter(options.getVectorSearchFilter(), recordDefinition); + + String knn = String.format("%s=>[KNN $K @%s $BLOB AS %s]", filter, + vectorField.getEffectiveStorageName(), VECTOR_SCORE_FIELD); + + FTSearchParams searchParams = new FTSearchParams() + .addParam("K", options.getTop() + options.getSkip()) + .addParam("BLOB", convertListToByteArray(vector)) + .limit(options.getSkip(), options.getTop()) + .sortBy(VECTOR_SCORE_FIELD, SortingOrder.ASC) + .dialect(2); + + // For hash set storage is possible to select what fields to return without them being filterable + if (storageType == RedisStorageType.HASH_SET) { + // We also need to tell Redis to return the fields without decoding them + // Vector fields specially need to be returned as raw bytes + for (VectorStoreRecordDataField dataField : recordDefinition.getDataFields()) { + searchParams.returnField(dataField.getEffectiveStorageName(), false); + } + if (options.isIncludeVectors()) { + for (VectorStoreRecordVectorField v : recordDefinition.getVectorFields()) { + searchParams.returnField(v.getEffectiveStorageName(), false); + } + } + + // Also, return the score field, this can be decoded. + searchParams.returnField(VECTOR_SCORE_FIELD, true); + } + + return Pair.of(knn, searchParams); + } + + /** + * Converts a list of floats to a byte array. + * @param embeddings the embeddings + * @return the byte array + */ + public static byte[] convertListToByteArray(List embeddings) { + ByteBuffer bytes = ByteBuffer.allocate(Float.BYTES * embeddings.size()); + bytes.order(ByteOrder.LITTLE_ENDIAN); + embeddings.iterator().forEachRemaining(bytes::putFloat); + return bytes.array(); + } + + /** + * Converts a byte array to a list of floats. + * @param bytes the byte array + * @return the list of floats + */ + public static List convertByteArrayToList(byte[] bytes) { + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + List embeddings = new java.util.ArrayList<>(); + while (buffer.hasRemaining()) { + embeddings.add(buffer.getFloat()); + } + return embeddings; + } + + /** + * Gets the filter string for the given vector search filter and record definition. + * + * @param filter The filter to get the filter string for. + * @param recordDefinition The record definition to get the filter string for. + * @return The filter string. + */ + @Override + public String getFilter(VectorSearchFilter filter, + VectorStoreRecordDefinition recordDefinition) { + if (filter == null + || filter.getFilterClauses().isEmpty()) { + return "*"; + } + + return String.format("(%s)", + filter.getFilterClauses().stream().map(filterClause -> { + if (filterClause instanceof EqualToFilterClause) { + EqualToFilterClause equalToFilterClause = (EqualToFilterClause) filterClause; + return getEqualToFilter(new EqualToFilterClause( + recordDefinition.getField(equalToFilterClause.getFieldName()) + .getEffectiveStorageName(), + equalToFilterClause.getValue())); + } else { + throw new SKException("Unsupported filter clause type '" + + filterClause.getClass().getSimpleName() + "'."); + } + }).collect(Collectors.joining(" "))); + } + + /** + * Gets the filter string for the given equal to filter clause. + * + * @param filterClause The equal to filter clause to get the filter string for. + * @return The filter string. + */ + @Override + public String getEqualToFilter(EqualToFilterClause filterClause) { + String fieldName = validateFieldName(filterClause.getFieldName()); + Object value = filterClause.getValue(); + String formattedValue; + + if (value instanceof String) { + formattedValue = String.format("\"%s\"", escapeRedisString((String) value)); + } else if (value instanceof Number) { + formattedValue = String.format("[%s %s]", value, value); + } else { + throw new SKException("Unsupported filter value type '" + + value.getClass().getSimpleName() + "'."); + } + + return String.format("@%s:%s", fieldName, formattedValue); + } + + /** + * Gets the filter string for the given any tag equal to filter clause. + * + * @param filterClause The any tag equal to filter clause to get the filter string for. + * @return The filter string. + */ + @Override + public String getAnyTagEqualToFilter(AnyTagEqualToFilterClause filterClause) { + return String.format("@%s:\"%s\"", + validateFieldName(filterClause.getFieldName()), + escapeRedisString(filterClause.getValue().toString())); + } + + private String validateFieldName(String fieldName) { + if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { + return fieldName; + } + throw new SKException("Invalid field name: " + fieldName); + } + + private String escapeRedisString(String searchString) { + return searchString.replaceAll("([,.<>{}\\[\\]\"':;!@#$%^&*()\\-+=~|\\\\/?\\s])", "\\\\$1"); + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreOptions.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreOptions.java similarity index 64% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreOptions.java rename to data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreOptions.java index 9b70fb498..730930278 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreOptions.java +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreOptions.java @@ -1,20 +1,30 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.redis; +package com.microsoft.semantickernel.data.redis; + +import com.microsoft.semantickernel.exceptions.SKException; import javax.annotation.Nonnull; import javax.annotation.Nullable; +/** + * Options for the Redis vector store. + */ public class RedisVectorStoreOptions { @Nullable private final RedisVectorStoreRecordCollectionFactory vectorStoreRecordCollectionFactory; + @Nonnull + private final RedisStorageType storageType; + /** * Creates a new instance of the Redis vector store options. - * + * @param storageType The storage type. * @param vectorStoreRecordCollectionFactory The vector store record collection factory. */ public RedisVectorStoreOptions( + @Nonnull RedisStorageType storageType, @Nullable RedisVectorStoreRecordCollectionFactory vectorStoreRecordCollectionFactory) { + this.storageType = storageType; this.vectorStoreRecordCollectionFactory = vectorStoreRecordCollectionFactory; } @@ -22,7 +32,7 @@ public RedisVectorStoreOptions( * Creates a new instance of the Redis vector store options. */ public RedisVectorStoreOptions() { - this(null); + this(RedisStorageType.JSON, null); } /** @@ -44,12 +54,24 @@ public RedisVectorStoreRecordCollectionFactory getVectorStoreRecordCollectionFac return vectorStoreRecordCollectionFactory; } + /** + * Gets the storage type. + * + * @return the storage type + */ + @Nonnull + public RedisStorageType getStorageType() { + return storageType; + } + /** * Builder for Redis vector store options. */ public static class Builder { @Nullable private RedisVectorStoreRecordCollectionFactory vectorStoreRecordCollectionFactory; + @Nullable + private RedisStorageType storageType; /** * Sets the vector store record collection factory. @@ -63,13 +85,28 @@ public Builder withVectorStoreRecordCollectionFactory( return this; } + /** + * Sets the storage type. + * + * @param storageType The storage type. + * @return The updated builder instance. + */ + public Builder withStorageType(RedisStorageType storageType) { + this.storageType = storageType; + return this; + } + /** * Builds the options. * * @return The options. */ public RedisVectorStoreOptions build() { - return new RedisVectorStoreOptions(vectorStoreRecordCollectionFactory); + if (storageType == null) { + throw new SKException("storageType is required"); + } + + return new RedisVectorStoreOptions(storageType, vectorStoreRecordCollectionFactory); } } } diff --git a/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreRecordCollectionFactory.java b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreRecordCollectionFactory.java new file mode 100644 index 000000000..bb1ab9a4b --- /dev/null +++ b/data/semantickernel-data-redis/src/main/java/com/microsoft/semantickernel/data/redis/RedisVectorStoreRecordCollectionFactory.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.redis; + +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import redis.clients.jedis.JedisPooled; + +/** + * Factory for creating Redis vector store record collections. + */ +public interface RedisVectorStoreRecordCollectionFactory { + + /** + * Creates a new vector store record collection. + * + * @param client The Redis client. + * @param collectionName The name of the collection. + * @param recordClass The class type of the record. + * @param recordDefinition The record definition. + * @param The type of the records in the collection. + * @return The collection. + */ + VectorStoreRecordCollection createVectorStoreRecordCollection( + JedisPooled client, + String collectionName, + Class recordClass, + VectorStoreRecordDefinition recordDefinition); +} diff --git a/data/semantickernel-data-sqlite/pom.xml b/data/semantickernel-data-sqlite/pom.xml new file mode 100644 index 000000000..7b4faa3de --- /dev/null +++ b/data/semantickernel-data-sqlite/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../../pom.xml + + + com.microsoft.semantic-kernel + semantickernel-data-sqlite + Semantic Kernel SQLite JDBC driver connector + Provides a SQLite connector for the Semantic Kernel + + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-data-jdbc + + + com.microsoft.semantic-kernel + semantickernel-api-data + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + + com.fasterxml.jackson.core + jackson-databind + compile + + + com.fasterxml.jackson.core + jackson-core + compile + + + com.github.spotbugs + spotbugs-annotations + + + org.xerial + sqlite-jdbc + 3.53.0.0 + + + \ No newline at end of file diff --git a/data/semantickernel-data-sqlite/src/main/java/com/microsoft/semantickernel/data/jdbc/sqlite/SQLiteVectorStoreQueryProvider.java b/data/semantickernel-data-sqlite/src/main/java/com/microsoft/semantickernel/data/jdbc/sqlite/SQLiteVectorStoreQueryProvider.java new file mode 100644 index 000000000..2d88702ee --- /dev/null +++ b/data/semantickernel-data-sqlite/src/main/java/com/microsoft/semantickernel/data/jdbc/sqlite/SQLiteVectorStoreQueryProvider.java @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.jdbc.sqlite; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDataField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import javax.annotation.Nonnull; +import javax.sql.DataSource; + +/** + * A query provider for a vector store in SQLite. + */ +public class SQLiteVectorStoreQueryProvider extends + JDBCVectorStoreQueryProvider implements SQLVectorStoreQueryProvider { + + private final DataSource dataSource; + private final ObjectMapper objectMapper; + + private SQLiteVectorStoreQueryProvider( + @Nonnull DataSource dataSource, + @Nonnull String collectionsTable, + @Nonnull String prefixForCollectionTables, + @Nonnull ObjectMapper objectMapper) { + super(dataSource, collectionsTable, prefixForCollectionTables); + this.dataSource = dataSource; + this.objectMapper = objectMapper; + } + + /** + * Creates a new builder. + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + private void setUpsertStatementValues(PreparedStatement statement, Object record, + List fields) { + JsonNode jsonNode = objectMapper.valueToTree(record); + + for (int i = 0; i < fields.size(); ++i) { + VectorStoreRecordField field = fields.get(i); + try { + JsonNode valueNode = jsonNode.get(field.getEffectiveStorageName()); + + if (field instanceof VectorStoreRecordVectorField) { + // Convert the vector field to a string + if (!field.getFieldType().equals(String.class)) { + statement.setObject(i + 1, objectMapper.writeValueAsString(valueNode)); + continue; + } + } else if (field instanceof VectorStoreRecordDataField) { + // Convert List field to a string + if (field.getFieldType().equals(List.class)) { + statement.setObject(i + 1, objectMapper.writeValueAsString(valueNode)); + continue; + } + } + + statement.setObject(i + 1, + objectMapper.convertValue(valueNode, field.getFieldType())); + } catch (SQLException | JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Upserts records into the collection. + * @param collectionName the collection name + * @param records the records to upsert + * @param recordDefinition the record definition + * @param options the upsert options + * @throws SKException if the upsert fails + */ + @Override + @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") // SQL query is generated dynamically with valid identifiers + public void upsertRecords(String collectionName, List records, + VectorStoreRecordDefinition recordDefinition, UpsertRecordOptions options) { + List fields = recordDefinition.getAllFields(); + + String query = formatQuery("INSERT OR REPLACE INTO %s (%s) VALUES (%s)", + getCollectionTableName(collectionName), + getQueryColumnsFromFields(fields), + getWildcardString(fields.size())); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + for (Object record : records) { + setUpsertStatementValues(statement, record, recordDefinition.getAllFields()); + statement.addBatch(); + } + + statement.executeBatch(); + } catch (SQLException e) { + throw new SKException("Failed to upsert records", e); + } + } + + @Override + protected String getInsertCollectionQuery(String collectionsTable) { + return formatQuery( + "INSERT OR IGNORE INTO %s (collectionId) VALUES (?)", + validateSQLidentifier(collectionsTable)); + } + + /** + * A builder for {@code SQLiteVectorStoreQueryProvider}. + */ + public static class Builder + extends JDBCVectorStoreQueryProvider.Builder { + private DataSource dataSource; + private String collectionsTable = DEFAULT_COLLECTIONS_TABLE; + private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; + private ObjectMapper objectMapper = new ObjectMapper(); + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withDataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + /** + * Sets the collections table name. + * @param collectionsTable the collections table name + * @return the builder + */ + public Builder withCollectionsTable(String collectionsTable) { + this.collectionsTable = validateSQLidentifier(collectionsTable); + return this; + } + + /** + * Sets the prefix for collection tables. + * @param prefixForCollectionTables the prefix for collection tables + * @return the builder + */ + public Builder withPrefixForCollectionTables(String prefixForCollectionTables) { + this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); + return this; + } + + /** + * Sets the object mapper. + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + public SQLiteVectorStoreQueryProvider build() { + if (dataSource == null) { + throw new SKException("DataSource is required"); + } + + return new SQLiteVectorStoreQueryProvider(dataSource, collectionsTable, + prefixForCollectionTables, objectMapper); + } + } +} diff --git a/mvnw b/mvnw index 8d937f4c1..19529ddf8 100755 --- a/mvnw +++ b/mvnw @@ -19,290 +19,241 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.2.0 -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false +# OS specific support. +native_path() { printf %s\\n "$1"; } case "$(uname)" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME - else - JAVA_HOME="/Library/Java/Home"; export JAVA_HOME - fi - fi - ;; +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; esac -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=$(java-config --jre-home) - fi -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --unix "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --unix "$CLASSPATH") -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && - JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="$(which javac)" - if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=$(which readlink) - if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then - if $darwin ; then - javaHome="$(dirname "\"$javaExecutable\"")" - javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" - else - javaExecutable="$(readlink -f "\"$javaExecutable\"")" - fi - javaHome="$(dirname "\"$javaExecutable\"")" - javaHome=$(expr "$javaHome" : '\(.*\)/bin') - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi fi else - JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi +} - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=$(cd "$wdir/.." || exit 1; pwd) - fi - # end of workaround +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done - printf '%s' "$(cd "$basedir" || exit 1; pwd)" + printf %x\\n $h } -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - # Remove \r in case we run on Windows within Git Bash - # and check out the repository with auto CRLF management - # enabled. Otherwise, we may read lines that are delimited with - # \r\n and produce $'-Xarg\r' rather than -Xarg due to word - # splitting rules. - tr -s '\r\n' ' ' < "$1" - fi +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 } -log() { - if [ "$MVNW_VERBOSE" = true ]; then - printf '%s\n' "$1" - fi +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } -BASE_DIR=$(find_maven_basedir "$(dirname "$0")") -if [ -z "$BASE_DIR" ]; then - exit 1; +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR -log "$MAVEN_PROJECTBASEDIR" +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" -if [ -r "$wrapperJarPath" ]; then - log "Found $wrapperJarPath" +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT else - log "Couldn't find $wrapperJarPath, downloading it ..." + die "cannot create temp dir" +fi - if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - fi - while IFS="=" read -r key value; do - # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) - safeValue=$(echo "$value" | tr -d '\r') - case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; - esac - done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" - log "Downloading from: $wrapperUrl" +mkdir -p -- "${MAVEN_HOME%/*}" - if $cygwin; then - wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") - fi +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - if command -v wget > /dev/null; then - log "Found wget ... using wget" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - log "Found curl ... using curl" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - else - curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - fi - else - log "Falling back to using Java to download" - javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" - javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaSource=$(cygpath --path --windows "$javaSource") - javaClass=$(cygpath --path --windows "$javaClass") - fi - if [ -e "$javaSource" ]; then - if [ ! -e "$javaClass" ]; then - log " - Compiling MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/javac" "$javaSource") - fi - if [ -e "$javaClass" ]; then - log " - Running MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" - fi - fi - fi +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -########################################################################################## -# End of extension -########################################################################################## -# If specified, validate the SHA-256 sum of the Maven wrapper jar file -wrapperSha256Sum="" -while IFS="=" read -r key value; do - case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; - esac -done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" -if [ -n "$wrapperSha256Sum" ]; then - wrapperSha256Result=false - if command -v sha256sum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then - wrapperSha256Result=true +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true fi - elif command -v shasum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then - wrapperSha256Result=true + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." - echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi - if [ $wrapperSha256Result = false ]; then - echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 - echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 - echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 exit 1 fi fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --windows "$CLASSPATH") - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -# shellcheck disable=SC2086 # safe args -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index c4586b564..249bdf382 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,3 +1,4 @@ +<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -18,188 +19,131 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.2.0 -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir +@REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Optional ENV vars -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %WRAPPER_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file -SET WRAPPER_SHA_256_SUM="" -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) -IF NOT %WRAPPER_SHA_256_SUM%=="" ( - powershell -Command "&{"^ - "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ - "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ - " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ - " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ - " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ - " exit 1;"^ - "}"^ - "}" - if ERRORLEVEL 1 goto error -) - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index a846ccabb..fd834bd30 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.microsoft.semantic-kernel semantickernel-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT pom https://www.github.com/microsoft/semantic-kernel @@ -14,66 +14,79 @@ Parent pom for the Semantic Kernel Project - 1.0.0-beta.8 - 10.13.0 + 1.0.0-beta.16 + 13.4.2 0.10.21 + false 2.19.1 1.17.0 1.6.0 - 5.10.2 - 2.22.1 + 5.11.3 + 2.25.4 3.1.0 2.12.1 - 3.3.1 - 3.3.2 - 3.12.1 + 3.5.0 + 3.4.0 + 3.13.0 8 8 8 - 3.6.1 - 3.1.1 + 3.8.0 + 3.1.3 1.0 - 3.4.1 - 3.1.1 - 3.1.1 - 0.8.11 - 3.3.0 - 3.6.3 - 3.3.2 + 3.5.0 + 3.5.0 + 3.1.3 + 0.8.12 + 3.4.2 + 3.10.1 + 3.5.0 2.4.0 - 3.21.2 - 3.5.0 + 3.28.0 + 3.8.0 0.16.1 - 3.0.1 + 3.1.1 3.3.1 - 4.0.0-M13 - 3.5.1 - 3.3.0 - 4.8.3.1 + 4.0.0-M16 + 3.6.0 + 3.3.1 + 4.8.6.5 2.38.0 - 3.2.5 - 3.9.3 - 2.16.2 - 5.10.0 + 3.5.1 + 3.9.9 + 2.17.1 + 5.14.2 0.9.1 - 6.55.0 + 7.23.0 UTF-8 microsoft/semantic-kernel git@github.com:${project.github.repository}.git - 4.8.3 - false + 4.9.8 semantickernel-bom - semantickernel-api semantickernel-experimental - aiservices/openai aiservices/google aiservices/huggingface + data/semantickernel-data-azureaisearch + data/semantickernel-data-jdbc + data/semantickernel-data-redis + data/semantickernel-data-mysql + data/semantickernel-data-hsqldb + data/semantickernel-data-postgres + data/semantickernel-data-sqlite + data/semantickernel-data-oracle + agents/semantickernel-agents-core + semantickernel-api-data + semantickernel-api-exceptions + semantickernel-api-builders + semantickernel-api-textembedding-services + semantickernel-api-localization + semantickernel-api-ai-services @@ -131,6 +144,36 @@ semantickernel-connectors-ai-openai ${project.version} + + com.microsoft.semantic-kernel + semantickernel-api-builders + ${project.version} + provided + + + com.microsoft.semantic-kernel + semantickernel-api-data + ${project.version} + provided + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + ${project.version} + provided + + + com.microsoft.semantic-kernel + semantickernel-api-localization + ${project.version} + provided + + + com.microsoft.semantic-kernel + semantickernel-api-textembedding-services + ${project.version} + provided + com.microsoft.semantic-kernel.extensions semantickernel-sequentialplanner-extension @@ -141,12 +184,89 @@ semantickernel-actionplanner-extension ${project.version} + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + ${project.version} + provided + + com.github.spotbugs spotbugs-annotations ${spotbugs.version} + + + org.wiremock + wiremock + 3.13.2 + test + + + org.mockito + mockito-junit-jupiter + 5.23.0 + test + + + + org.testcontainers + junit-jupiter + 1.21.4 + test + + + org.testcontainers + postgresql + 1.21.4 + test + + + org.testcontainers + mysql + 1.21.4 + test + + + + + com.microsoft.semantic-kernel + semantickernel-api-builders + provided + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + provided + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-api-localization + provided + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-api-textembedding-services + provided + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + provided + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-api-data + provided + ${project.version} + @@ -198,6 +318,7 @@ ${maven.compiler.release} ${maven.compiler.release} 8 + -Xlint:unchecked @@ -282,6 +403,11 @@ pmd-jsp ${pmd.version} + + org.apache.maven.plugins + maven-pmd-plugin + ${maven.pmd-plugin.version} + @@ -377,7 +503,7 @@ org.codehaus.mojo animal-sniffer-maven-plugin - 1.23 + 1.24 android @@ -422,6 +548,15 @@ update-project-license + + add-third-party + + add-third-party + + + compile,runtime + + @@ -702,6 +837,15 @@ api-test + + skip-container-tests + + false + + + true + + release diff --git a/samples/pom.xml b/samples/pom.xml index a153ef2fe..6ddaea22d 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml diff --git a/samples/semantickernel-concepts/pom.xml b/samples/semantickernel-concepts/pom.xml index 673abe6d2..99d85497c 100644 --- a/samples/semantickernel-concepts/pom.xml +++ b/samples/semantickernel-concepts/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-samples-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml b/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml index dcf929996..8d42b664b 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-concepts - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -26,10 +26,34 @@ + + io.opentelemetry.instrumentation + opentelemetry-reactor-3.1 + 2.26.1-alpha + com.microsoft.semantic-kernel semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-data-azureaisearch + + + com.microsoft.semantic-kernel + semantickernel-data-jdbc + + + com.microsoft.semantic-kernel + semantickernel-data-redis + + + + com.microsoft.semantic-kernel + semantickernel-agents-core + + com.microsoft.semantic-kernel semantickernel-experimental @@ -81,18 +105,86 @@ com.microsoft.semantic-kernel semantickernel-aiservices-google + + com.microsoft.semantic-kernel + semantickernel-text-splitter-plugin + ${project.version} + + + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + com.microsoft.semantic-kernel + semantickernel-api-localization + + + com.microsoft.semantic-kernel + semantickernel-api-textembedding-services + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + com.microsoft.semantic-kernel + semantickernel-api-data + + + + org.apache.pdfbox + pdfbox + 3.0.7 + com.google.cloud google-cloud-vertexai - 1.1.0 + 1.52.0 compile - mysql - mysql-connector-java - 8.0.33 + com.mysql + mysql-connector-j + 9.6.0 + + + com.github.victools + jsonschema-generator + + + com.github.victools + jsonschema-module-jackson + + + com.microsoft.semantic-kernel + semantickernel-data-jdbc + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-learn-resources + ${project.version} + compile + + + com.microsoft.semantic-kernel + semantickernel-data-postgres + ${project.version} + compile + + + com.microsoft.semantic-kernel + semantickernel-data-oracle + ${project.version} + compile @@ -143,6 +235,7 @@ com.microsoft.semantickernel.samples.syntaxexamples.${sample} + false diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/App.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/App.java index 08a8f3b52..21a4b1c8c 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/App.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/App.java @@ -70,9 +70,7 @@ public static void main(String[] args) throws Exception { ChatCompletionService.class); ContextVariableTypes - .addGlobalConverter(ContextVariableTypeConverter.builder(LightModel.class) - .toPromptString(new Gson()::toJson) - .build()); + .addGlobalConverter(new LightModelTypeConverter()); KernelHooks hook = new KernelHooks(); @@ -99,9 +97,7 @@ public static void main(String[] args) throws Exception { InvocationContext invocationContext = new Builder() .withReturnMode(InvocationReturnMode.LAST_MESSAGE_ONLY) .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) - .withContextVariableConverter(ContextVariableTypeConverter.builder(LightModel.class) - .toPromptString(new Gson()::toJson) - .build()) + .withContextVariableConverter(new LightModelTypeConverter()) .build(); // Create a history to store the conversation diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightModel.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightModel.java index 072763a91..e7958507b 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightModel.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightModel.java @@ -1,10 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.samples.demos.lights; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + public class LightModel { + @JsonPropertyDescription("The unique identifier of the light") private int id; + + @JsonPropertyDescription("The name of the light") private String name; + + @JsonPropertyDescription("The state of the light") private Boolean isOn; public LightModel(int id, String name, Boolean isOn) { diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightModelTypeConverter.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightModelTypeConverter.java new file mode 100644 index 000000000..2752eb624 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightModelTypeConverter.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.demos.lights; + +import com.google.gson.Gson; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; + +public class LightModelTypeConverter extends ContextVariableTypeConverter { + private static final Gson gson = new Gson(); + + public LightModelTypeConverter() { + super( + LightModel.class, + obj -> { + if (obj instanceof String) { + return gson.fromJson((String) obj, LightModel.class); + } else { + return gson.fromJson(gson.toJson(obj), LightModel.class); + } + }, + (types, lightModel) -> gson.toJson(lightModel), + json -> gson.fromJson(json, LightModel.class)); + } +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightsPlugin.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightsPlugin.java index 55d70b021..398a8d16d 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightsPlugin.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/demos/lights/LightsPlugin.java @@ -24,10 +24,21 @@ public List getLights() { return lights; } + @DefineKernelFunction(name = "add_light", description = "Adds a new light") + public String addLight( + @KernelFunctionParameter(name = "newLight", description = "new Light Details", type = LightModel.class) LightModel light) { + if (light != null) { + System.out.println("Adding light " + light.getName()); + lights.add(light); + return "Light added"; + } + return "Light failed to added"; + } + @DefineKernelFunction(name = "change_state", description = "Changes the state of the light") public LightModel changeState( - @KernelFunctionParameter(name = "id", description = "The ID of the light to change") int id, - @KernelFunctionParameter(name = "isOn", description = "The new state of the light") boolean isOn) { + @KernelFunctionParameter(name = "id", description = "The ID of the light to change", type = int.class) int id, + @KernelFunctionParameter(name = "isOn", description = "The new state of the light", type = boolean.class) boolean isOn) { System.out.println("Changing light " + id + " " + isOn); Optional light = lights.stream() .filter(l -> l.getId() == id) diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/ConversationSummaryPlugin.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/ConversationSummaryPlugin.java index 089a4941e..f61d1d873 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/ConversationSummaryPlugin.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/ConversationSummaryPlugin.java @@ -5,7 +5,7 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; import com.microsoft.semantickernel.text.TextChunker; @@ -71,7 +71,7 @@ private static Mono processAsync(KernelFunction func, String inp // The first parameter is the input text. return func.invokeAsync(kernel) .withArguments( - new KernelFunctionArguments.Builder() + KernelArguments.builder() .withInput(paragraph) .build()) .withResultType( diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java new file mode 100644 index 000000000..0f8065ee1 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.plugins.github; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public abstract class GitHubModel { + public final static ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + @Override + public String toString() { + try { + return objectMapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public static class User extends GitHubModel { + @JsonProperty("login") + private String login; + @JsonProperty("id") + private long id; + @JsonProperty("name") + private String name; + @JsonProperty("company") + private String company; + @JsonProperty("html_url") + private String url; + + @JsonCreator + public User(@JsonProperty("login") String login, + @JsonProperty("id") long id, + @JsonProperty("name") String name, + @JsonProperty("company") String company, + @JsonProperty("html_url") String url) { + this.login = login; + this.id = id; + this.name = name; + this.company = company; + this.url = url; + } + + public String getLogin() { + return login; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getCompany() { + return company; + } + + public String getUrl() { + return url; + } + } + + public static class Repository extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("full_name") + private String name; + @JsonProperty("description") + private String description; + @JsonProperty("html_url") + private String url; + + @JsonCreator + public Repository(@JsonProperty("id") long id, + @JsonProperty("full_name") String name, + @JsonProperty("description") String description, + @JsonProperty("html_url") String url) { + this.id = id; + this.name = name; + this.description = description; + this.url = url; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getUrl() { + return url; + } + + @Override + public String toString() { + try { + return objectMapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + public static class Issue extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("number") + private long number; + @JsonProperty("title") + private String title; + @JsonProperty("state") + private String state; + @JsonProperty("html_url") + private String url; + @JsonProperty("labels") + private Label[] labels; + @JsonProperty("created_at") + private String createdAt; + @JsonProperty("closed_at") + private String closedAt; + + @JsonCreator + public Issue(@JsonProperty("id") long id, + @JsonProperty("number") long number, + @JsonProperty("title") String title, + @JsonProperty("state") String state, + @JsonProperty("html_url") String url, + @JsonProperty("labels") Label[] labels, + @JsonProperty("created_at") String createdAt, + @JsonProperty("closed_at") String closedAt) { + this.id = id; + this.number = number; + this.title = title; + this.state = state; + this.url = url; + this.labels = labels; + this.createdAt = createdAt; + this.closedAt = closedAt; + } + + public long getId() { + return id; + } + + public long getNumber() { + return number; + } + + public String getTitle() { + return title; + } + + public String getState() { + return state; + } + + public String getUrl() { + return url; + } + + public Label[] getLabels() { + return labels; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getClosedAt() { + return closedAt; + } + } + + public static class IssueDetail extends Issue { + @JsonProperty("body") + private String body; + + @JsonCreator + public IssueDetail(@JsonProperty("id") long id, + @JsonProperty("number") long number, + @JsonProperty("title") String title, + @JsonProperty("state") String state, + @JsonProperty("html_url") String url, + @JsonProperty("labels") Label[] labels, + @JsonProperty("created_at") String createdAt, + @JsonProperty("closed_at") String closedAt, + @JsonProperty("body") String body) { + super(id, number, title, state, url, labels, createdAt, closedAt); + this.body = body; + } + + public String getBody() { + return body; + } + } + + public static class Label extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("name") + private String name; + @JsonProperty("description") + private String description; + + @JsonCreator + public Label(@JsonProperty("id") long id, + @JsonProperty("name") String name, + @JsonProperty("description") String description) { + this.id = id; + this.name = name; + this.description = description; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + } +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java new file mode 100644 index 000000000..f0bddee10 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.plugins.github; + +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; +import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; + +import java.io.IOException; +import java.util.List; + +public class GitHubPlugin { + public static final String baseUrl = "https://api.github.com"; + private final String token; + + public GitHubPlugin(String token) { + this.token = token; + } + + @DefineKernelFunction(name = "get_user_info", description = "Get user information from GitHub", returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$User") + public Mono getUserProfileAsync() { + HttpClient client = createClient(); + + return makeRequestAsync(client, "/user") + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.User.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubUser", e); + } + }); + } + + @DefineKernelFunction(name = "get_repo_info", description = "Get repository information from GitHub", returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$Repository") + public Mono getRepositoryAsync( + @KernelFunctionParameter(name = "organization", description = "The name of the repository to retrieve information for") String organization, + @KernelFunctionParameter(name = "repo_name", description = "The name of the repository to retrieve information for") String repoName) { + HttpClient client = createClient(); + + return makeRequestAsync(client, String.format("/repos/%s/%s", organization, repoName)) + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.Repository.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubRepository", e); + } + }); + } + + @DefineKernelFunction(name = "get_issues", description = "Get issues from GitHub", returnType = "java.util.List") + public Mono> getIssuesAsync( + @KernelFunctionParameter(name = "organization", description = "The name of the organization to retrieve issues for") String organization, + @KernelFunctionParameter(name = "repo_name", description = "The name of the repository to retrieve issues for") String repoName, + @KernelFunctionParameter(name = "max_results", description = "The maximum number of issues to retrieve", required = false, defaultValue = "10", type = int.class) int maxResults, + @KernelFunctionParameter(name = "state", description = "The state of the issues to retrieve", required = false, defaultValue = "open") String state, + @KernelFunctionParameter(name = "assignee", description = "The assignee of the issues to retrieve", required = false) String assignee) { + HttpClient client = createClient(); + + String query = String.format("/repos/%s/%s/issues", organization, repoName); + query = buildQueryString(query, "state", state); + query = buildQueryString(query, "assignee", assignee); + query = buildQueryString(query, "per_page", String.valueOf(maxResults)); + + return makeRequestAsync(client, query) + .flatMap(json -> { + try { + GitHubModel.Issue[] issues = GitHubModel.objectMapper.readValue(json, + GitHubModel.Issue[].class); + return Mono.just(List.of(issues)); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubIssues", e); + } + }); + } + + @DefineKernelFunction(name = "get_issue_detail_info", description = "Get detail information of a single issue from GitHub", returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$IssueDetail") + public GitHubModel.IssueDetail getIssueDetailAsync( + @KernelFunctionParameter(name = "organization", description = "The name of the repository to retrieve information for") String organization, + @KernelFunctionParameter(name = "repo_name", description = "The name of the repository to retrieve information for") String repoName, + @KernelFunctionParameter(name = "issue_number", description = "The issue number to retrieve information for", type = int.class) int issueNumber) { + HttpClient client = createClient(); + + return makeRequestAsync(client, + String.format("/repos/%s/%s/issues/%d", organization, repoName, issueNumber)) + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.IssueDetail.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubIssue", e); + } + }).block(); + } + + private HttpClient createClient() { + return HttpClient.create() + .baseUrl(baseUrl) + .headers(headers -> { + headers.add("User-Agent", "request"); + headers.add("Accept", "application/vnd.github+json"); + headers.add("Authorization", "Bearer " + token); + headers.add("X-GitHub-Api-Version", "2022-11-28"); + }); + } + + private static String buildQueryString(String path, String param, String value) { + if (value == null || value.isEmpty() + || value.equals(KernelFunctionParameter.NO_DEFAULT_VALUE)) { + return path; + } + + return path + (path.contains("?") ? "&" : "?") + param + "=" + value; + } + + private Mono makeRequestAsync(HttpClient client, String path) { + return client + .get() + .uri(path) + .responseSingle((res, content) -> { + if (res.status().code() != 200) { + return Mono.error(new IllegalStateException("Request failed: " + res.status())); + } + return content.asString(); + }); + } +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example11_WebSearchQueries.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example11_WebSearchQueries.java index 63abdbccb..1386cc619 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example11_WebSearchQueries.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example11_WebSearchQueries.java @@ -4,7 +4,7 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.web.SearchUrlPlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; public class Example11_WebSearchQueries { @@ -19,7 +19,7 @@ public static void main(String[] args) { // Run var ask = "What's the largest building in Europe?"; - var kernelArguments = KernelFunctionArguments.builder() + var kernelArguments = KernelArguments.builder() .withVariable("query", ask) .build(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example20_HuggingFace.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example20_HuggingFace.java index a388c130e..14fccf0a0 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example20_HuggingFace.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example20_HuggingFace.java @@ -5,7 +5,7 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.aiservices.huggingface.HuggingFaceClient; import com.microsoft.semantickernel.aiservices.huggingface.services.HuggingFaceTextGenerationService; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; @@ -43,7 +43,7 @@ public static void runInferenceApiExampleAsync() { var result = kernel.invokeAsync(questionAnswerFunction) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("input", "What is New York?") .build()) .withResultType(String.class) diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example43_GetModelResult.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example43_GetModelResult.java index 9dc86a68f..7b1e5752e 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example43_GetModelResult.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example43_GetModelResult.java @@ -10,7 +10,7 @@ import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; @@ -61,7 +61,7 @@ public static void main(String[] args) { FunctionResult result = kernel.invokeAsync( myFunction) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("input", "travel") .build()) .block(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example57_KernelHooks.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example57_KernelHooks.java index 0203f66c8..ffa09a677 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example57_KernelHooks.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example57_KernelHooks.java @@ -24,7 +24,7 @@ import com.microsoft.semantickernel.hooks.PromptRenderedEvent; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.semanticfunctions.OutputVariable; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; @@ -133,7 +133,7 @@ private static void getUsageAsync(Kernel kernel) { String input = "I missed the F1 final race"; var result = kernel.invokeAsync(excuseFunction) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable("input", input) .build()) @@ -189,7 +189,7 @@ private static void getRenderedPromptAsync(Kernel kernel) { String input = "I missed the F1 final race"; var result = kernel.invokeAsync(excuseFunction) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable("input", input) .build()) @@ -235,7 +235,7 @@ private static void changingResultAsync(Kernel kernel) { // Invoke prompt to trigger execution hooks. var result = kernel.invokeAsync(writerFunction) .withArguments( - KernelFunctionArguments.builder().build()) + KernelArguments.builder().build()) .block(); System.out.println("Function Result: " + result.getResult()); } @@ -275,7 +275,7 @@ private static void beforeInvokeCancellationAsync(Kernel kernel) { // Invoke prompt to trigger execution hooks. var result = kernel.invokeAsync(writerFunction) .withArguments( - KernelFunctionArguments.builder().build()) + KernelArguments.builder().build()) .block(); System.out.println("Function Result: " + result.getResult()); } catch (Exception e) { @@ -312,7 +312,7 @@ private static void afterInvokeCancellationAsync(Kernel kernel) { // Invoke prompt to trigger execution hooks. try { var result = kernel.invokeAsync(secondFunction) - .withArguments(KernelFunctionArguments.builder().build()) + .withArguments(KernelArguments.builder().build()) .block(); System.out.println("Function Result: " + result.getResult()); } catch (Exception e) { @@ -359,7 +359,7 @@ private static void chatCompletionHook(Kernel kernel) { // Invoke prompt to trigger execution hooks. var result = kernel.invokeAsync(writerFunction) .withArguments( - KernelFunctionArguments.builder().build()) + KernelArguments.builder().build()) .block(); System.out.println("Function Result: " + result.getResult()); } catch (Exception e) { @@ -403,7 +403,7 @@ private static void invocationHook(Kernel kernel) { try { // Invoke prompt to trigger execution hooks. var result = kernel.invokeAsync(writerFunction) - .withArguments(KernelFunctionArguments.builder().build()) + .withArguments(KernelArguments.builder().build()) .addKernelHooks(kernelHooks) .block(); System.out.println("Function Result: " + result.getResult()); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example61_MultipleLLMs.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example61_MultipleLLMs.java index 8348ee517..0396d575f 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example61_MultipleLLMs.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example61_MultipleLLMs.java @@ -9,6 +9,7 @@ import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; @@ -73,7 +74,7 @@ public static void runByServiceIdAsync(Kernel kernel, String serviceId) { var prompt = "Hello AI, what can you do for me?"; - KernelFunctionArguments arguments = KernelFunctionArguments.builder().build(); + KernelArguments arguments = KernelArguments.builder().build(); KernelFunction func = KernelFunctionFromPrompt .builder() @@ -104,7 +105,7 @@ public static void runByModelIdAsync(Kernel kernel, String modelId) { .build()) .withOutputVariable("result", "java.lang.String") .build()) - .withArguments(KernelFunctionArguments.builder().build()) + .withArguments(KernelArguments.builder().build()) .block(); System.out.println(result.getResult()); @@ -136,7 +137,7 @@ public static void runByFirstModelIdAsync(Kernel kernel, String... modelIds) { .build(); var result = kernel.invokeAsync(function) - .withArguments(KernelFunctionArguments.builder().build()) + .withArguments(KernelArguments.builder().build()) .block(); System.out.println(result.getResult()); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example62_CustomAIServiceSelector.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example62_CustomAIServiceSelector.java index afa5b2e33..c3ebd6a0f 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example62_CustomAIServiceSelector.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example62_CustomAIServiceSelector.java @@ -9,7 +9,7 @@ import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.services.AIService; import com.microsoft.semantickernel.services.AIServiceCollection; @@ -50,7 +50,7 @@ public static void main(String[] args) { var openAIChatCompletion = OpenAIChatCompletion.builder() .withOpenAIAsyncClient(client) .withServiceId("AzureOpenAIChat") - .withModelId("gpt-35-turbo-2") + .withModelId("gpt-35-turbo") .build(); var textGenerationService = TextGenerationService.builder() @@ -67,7 +67,7 @@ public static void main(String[] args) { var prompt = "Hello AI, what can you do for me?"; - KernelFunctionArguments arguments = KernelFunctionArguments.builder().build(); + KernelArguments arguments = KernelArguments.builder().build(); KernelFunction func = KernelFunctionFromPrompt .builder() @@ -98,7 +98,7 @@ public AIServiceSelection trySelectAIService( @Nullable KernelFunction function, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, Map, AIService> services) { // Just get the first one diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example67_KernelStreaming.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example67_KernelStreaming.java index 36a0a4248..f6f686810 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example67_KernelStreaming.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example67_KernelStreaming.java @@ -1,10 +1,94 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.samples.syntaxexamples; -/* - * Currently the Java Semantic Kernel does not support streaming. If this is a feature you would like to see, please - * let us know by creating an issue on github or letting us know on Discord. - */ +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent; +import java.util.concurrent.CountDownLatch; + public class Example67_KernelStreaming { + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = System.getenv() + .getOrDefault("MODEL_ID", "gpt-35-turbo"); + + public static void main(String[] args) throws InterruptedException { + System.out.println("======== Open AI - ChatGPT Streaming ========"); + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + ChatCompletionService chatGPT = OpenAIChatCompletion.builder() + .withModelId(MODEL_ID) + .withOpenAIAsyncClient(client) + .build(); + + System.out.println("Chat content:"); + System.out.println("------------------------"); + + ChatHistory chatHistory = new ChatHistory("You are a librarian, expert about books"); + + // First user message + chatHistory.addUserMessage("Hi, I'm looking for book suggestions"); + messageOutput(chatHistory); + + GPTReply(chatGPT, chatHistory); + + chatHistory.addUserMessage( + "I love history and philosophy, I'd like to learn something new about Greece, any suggestion"); + messageOutput(chatHistory); + + GPTReply(chatGPT, chatHistory); + } + + private static void messageOutput(ChatHistory chatHistory) { + var message = chatHistory.getLastMessage().get(); + System.out.println(message.getAuthorRole() + ": " + message.getContent()); + System.out.println("------------------------"); + } + + private static void GPTReply(ChatCompletionService chatGPT, ChatHistory chatHistory) + throws InterruptedException { + var reply = chatGPT.getStreamingChatMessageContentsAsync(chatHistory, null, null); + System.out.print(AuthorRole.ASSISTANT + ": "); + + CountDownLatch cdl = new CountDownLatch(1); + reply + .groupBy(StreamingChatContent::getId) + .doOnComplete(() -> { + cdl.countDown(); + }) + .subscribe(group -> { + group + .filter(msg -> msg.getContent() != null) + .subscribe( + msg -> { + System.out.print(msg.getContent()); + }); + }); + + cdl.await(); + } + } diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example69_MutableKernelPlugin.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example69_MutableKernelPlugin.java index 23bd37787..591bab7eb 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example69_MutableKernelPlugin.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example69_MutableKernelPlugin.java @@ -3,6 +3,7 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.plugin.KernelPlugin; +import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; @@ -16,6 +17,7 @@ public class Example69_MutableKernelPlugin { */ public static void main(String[] args) throws NoSuchMethodException { System.out.println("======== Example69_MutableKernelPlugin ========"); + KernelPluginFactory.setTypeFilterEnable(false); KernelPlugin plugin = new KernelPlugin("Plugin", "Mutable plugin", null); plugin.addFunction(KernelFunction.createFromMethod( diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/RunAll.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/RunAll.java index a5b19fe83..44a979796 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/RunAll.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/RunAll.java @@ -6,6 +6,8 @@ import com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.Example33_Chat; import com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.Example44_MultiChatCompletion; import com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.Example63_ChatCompletionPrompts; +import com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema.Example_ChatWithResponseFormat; +import com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema.Example_ChatWithResponseFormatToolCall; import com.microsoft.semantickernel.samples.syntaxexamples.configuration.Example08_RetryHandler; import com.microsoft.semantickernel.samples.syntaxexamples.configuration.Example41_HttpClientUsage; import com.microsoft.semantickernel.samples.syntaxexamples.configuration.Example58_ConfigureExecutionSettings; @@ -17,9 +19,9 @@ import com.microsoft.semantickernel.samples.syntaxexamples.functions.Example59_OpenAIFunctionCalling; import com.microsoft.semantickernel.samples.syntaxexamples.functions.Example60_AdvancedMethodFunctions; import com.microsoft.semantickernel.samples.syntaxexamples.java.KernelFunctionYaml_Example; -import com.microsoft.semantickernel.samples.syntaxexamples.memory.AzureAISearch_DataStorage; -import com.microsoft.semantickernel.samples.syntaxexamples.plugins.Example10_DescribeAllPluginsAndFunctions; -import com.microsoft.semantickernel.samples.syntaxexamples.plugins.Example13_ConversationSummaryPlugin; +import com.microsoft.semantickernel.samples.syntaxexamples.memory.VectorStoreWithAzureAISearch; +import com.microsoft.semantickernel.samples.syntaxexamples.plugin.Example10_DescribeAllPluginsAndFunctions; +import com.microsoft.semantickernel.samples.syntaxexamples.plugin.Example13_ConversationSummaryPlugin; import com.microsoft.semantickernel.samples.syntaxexamples.template.Example06_TemplateLanguage; import com.microsoft.semantickernel.samples.syntaxexamples.template.Example56_TemplateMethodFunctionsWithMultipleArguments; import com.microsoft.semantickernel.samples.syntaxexamples.template.Example64_MultiplePromptTemplates; @@ -38,7 +40,7 @@ public class RunAll { public static void main(String[] args) { List mains = Arrays.asList( - AzureAISearch_DataStorage::main, + VectorStoreWithAzureAISearch::main, Example01_NativeFunctions::main, Example03_Arguments::main, Example05_InlineFunctionDefinition::main, @@ -66,7 +68,9 @@ public static void main(String[] args) { Example63_ChatCompletionPrompts::main, Example64_MultiplePromptTemplates::main, Example69_MutableKernelPlugin::main, - KernelFunctionYaml_Example::main); + KernelFunctionYaml_Example::main, + Example_ChatWithResponseFormat::main, + Example_ChatWithResponseFormatToolCall::main); Scanner scanner = new Scanner(System.in); mains.forEach(mainMethod -> { diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/agents/CompletionAgent.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/agents/CompletionAgent.java new file mode 100644 index 000000000..336406a33 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/agents/CompletionAgent.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.agents; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.agents.AgentInvokeOptions; +import com.microsoft.semantickernel.agents.AgentThread; +import com.microsoft.semantickernel.agents.chatcompletion.ChatCompletionAgent; +import com.microsoft.semantickernel.agents.chatcompletion.ChatHistoryAgentThread; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.functionchoice.FunctionChoiceBehavior; +import com.microsoft.semantickernel.implementation.templateengine.tokenizer.DefaultPromptTemplate; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.orchestration.ToolCallBehavior; +import com.microsoft.semantickernel.plugin.KernelPluginFactory; +import com.microsoft.semantickernel.samples.plugins.github.GitHubModel; +import com.microsoft.semantickernel.samples.plugins.github.GitHubPlugin; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; + +import java.util.List; +import java.util.Scanner; + +public class CompletionAgent { + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = System.getenv() + .getOrDefault("MODEL_ID", "gpt-4o"); + + private static final String GITHUB_PAT = System.getenv("GITHUB_PAT"); + + public static void main(String[] args) { + System.out.println("======== ChatCompletion Agent ========"); + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + System.out.println("------------------------"); + + ChatCompletionService chatCompletion = OpenAIChatCompletion.builder() + .withModelId(MODEL_ID) + .withOpenAIAsyncClient(client) + .build(); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chatCompletion) + .withPlugin(KernelPluginFactory.createFromObject(new GitHubPlugin(GITHUB_PAT), + "GitHubPlugin")) + .build(); + + InvocationContext invocationContext = InvocationContext.builder() + .withFunctionChoiceBehavior(FunctionChoiceBehavior.auto(true)) + .withContextVariableConverter(new ContextVariableTypeConverter<>( + GitHubModel.Issue.class, + o -> (GitHubModel.Issue) o, + o -> o.toString(), + s -> null)) + .build(); + + ChatCompletionAgent agent = ChatCompletionAgent.builder() + .withKernel(kernel) + .withKernelArguments( + KernelArguments.builder() + .withVariable("repository", "microsoft/semantic-kernel-java") + .withExecutionSettings(PromptExecutionSettings.builder() + .build()) + .build()) + .withInvocationContext(invocationContext) + .withTemplate( + DefaultPromptTemplate.build( + PromptTemplateConfig.builder() + .withTemplate( + """ + You are an agent designed to query and retrieve information from a single GitHub repository in a read-only manner. + You are also able to access the profile of the active user. + + Use the current date and time to provide up-to-date details or time-sensitive responses. + + The repository you are querying is a public repository with the following name: {{$repository}} + + The current date and time is: {{$now}}. + """) + .build())) + .build(); + + AgentThread agentThread = new ChatHistoryAgentThread(); + Scanner scanner = new Scanner(System.in); + + while (true) { + System.out.print("> "); + + String input = scanner.nextLine(); + if (input.equalsIgnoreCase("exit")) { + break; + } + + var message = new ChatMessageContent<>(AuthorRole.USER, input); + KernelArguments arguments = KernelArguments.builder() + .withVariable("now", System.currentTimeMillis()) + .build(); + + var response = agent.invokeAsync( + message, + agentThread, + AgentInvokeOptions.builder() + .withKernelArguments(arguments) + .build()) + .block().get(0); + + System.out.println("> " + response.getMessage()); + agentThread = response.getThread(); + } + } +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/Example30_ChatWithPrompts.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/Example30_ChatWithPrompts.java index 1357cc476..04a7a34fe 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/Example30_ChatWithPrompts.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/Example30_ChatWithPrompts.java @@ -11,7 +11,7 @@ import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.TimePlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateFactory; import com.microsoft.semantickernel.services.ServiceNotFoundException; @@ -82,7 +82,7 @@ public static void main(String[] args) throws FileNotFoundException, ServiceNotF // Adding required arguments referenced by the prompt templates. - var arguments = KernelFunctionArguments + var arguments = KernelArguments .builder() .withVariable("selectedText", selectedText) .withVariable("startTime", DateTimeFormatter.ofPattern("hh:mm:ss a zz").format( diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Example_ChatWithResponseFormat.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Example_ChatWithResponseFormat.java new file mode 100644 index 000000000..8a5d02334 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Example_ChatWithResponseFormat.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.orchestration.FunctionResult; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.orchestration.responseformat.JsonSchemaResponseFormat; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; + +public class Example_ChatWithResponseFormat { + + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = System.getenv() + .getOrDefault("MODEL_ID", "gpt-4o"); + + public static void main(String[] args) throws InterruptedException, JsonProcessingException { + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + ChatCompletionService openAIChatCompletion = OpenAIChatCompletion.builder() + .withOpenAIAsyncClient(client) + .withModelId(MODEL_ID) + .build(); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, openAIChatCompletion) + .build(); + + PromptExecutionSettings promptExecutionSettings = PromptExecutionSettings.builder() + .withResponseFormat( + JsonSchemaResponseFormat.builder() + .setResponseFormat(Pet.class) + .setName("Pet") + .build()) + .withMaxCompletionTokensEnable(true) + .build(); + + FunctionResult response = kernel + .invokePromptAsync("Generate an example pet that can be used in test code") + .withResultTypeAutoConversion(Pet.class) + .withPromptExecutionSettings(promptExecutionSettings) + .block(); + + System.out.println(new ObjectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(response.getResult())); + } + +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Example_ChatWithResponseFormatToolCall.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Example_ChatWithResponseFormatToolCall.java new file mode 100644 index 000000000..a3126eff5 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Example_ChatWithResponseFormatToolCall.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.orchestration.FunctionResult; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.orchestration.ToolCallBehavior; +import com.microsoft.semantickernel.plugin.KernelPluginFactory; +import com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema.Pet.AnimalType; +import com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema.Pet.Weight; +import com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema.Pet.WeightUnit; +import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; +import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; + +public class Example_ChatWithResponseFormatToolCall { + + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = System.getenv() + .getOrDefault("MODEL_ID", "gpt-4o"); + + public static void main(String[] args) throws InterruptedException, JsonProcessingException { + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + ChatCompletionService openAIChatCompletion = OpenAIChatCompletion.builder() + .withOpenAIAsyncClient(client) + .withModelId(MODEL_ID) + .build(); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, openAIChatCompletion) + .withPlugin(KernelPluginFactory.createFromObject(new PetStore(), "PetStore")) + .build(); + + PromptExecutionSettings promptExecutionSettings = PromptExecutionSettings.builder() + .withJsonSchemaResponseFormat(Pet.class) + .withMaxCompletionTokensEnable(true) + .build(); + + FunctionResult response = kernel.invokePromptAsync("Get pet with id 1234") + .withResultTypeAutoConversion(Pet.class) + .withPromptExecutionSettings(promptExecutionSettings) + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .block(); + + System.out.println(new ObjectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(response.getResult())); + } + + public static class PetStore { + + @DefineKernelFunction(name = "getPetById", returnType = "com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema.Pet") + public Pet getPetById( + @KernelFunctionParameter(name = "id") String id) { + if (id.equals("1234")) { + return new Pet( + "Test name", + AnimalType.CAT, + 5, + new Weight(5.0, WeightUnit.KG)); + } + return null; + } + } + +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Pet.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Pet.java new file mode 100644 index 000000000..b88a8e53f --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/chatcompletion/responseschema/Pet.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.chatcompletion.responseschema; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Pet { + + private final String name; + private final AnimalType type; + private final int age; + private final Weight weight; + + public static enum AnimalType { + CAT, DOG, FISH + } + + public static enum WeightUnit { + KG, LB + } + + public static class Weight { + + private final double value; + private final WeightUnit unit; + + @JsonCreator + public Weight( + @JsonProperty("value") double value, + @JsonProperty("unit") WeightUnit unit) { + this.value = value; + this.unit = unit; + } + + public double getValue() { + return value; + } + + public WeightUnit getUnit() { + return unit; + } + } + + @JsonCreator + public Pet( + @JsonProperty("name") String name, + @JsonProperty("type") AnimalType type, + @JsonProperty("age") int age, + @JsonProperty("weight") Weight weight) { + this.name = name; + this.type = type; + this.age = age; + this.weight = weight; + } + + public String getName() { + return name; + } + + public AnimalType getType() { + return type; + } + + public int getAge() { + return age; + } + + public Weight getWeight() { + return weight; + } +} \ No newline at end of file diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example03_Arguments.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example03_Arguments.java index 2096386dd..7db37899c 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example03_Arguments.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example03_Arguments.java @@ -5,7 +5,7 @@ import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; import java.util.Locale; @@ -24,7 +24,7 @@ public static void main(String[] args) { KernelPlugin functionCollection = KernelPluginFactory .createFromObject(new StaticTextPlugin(), "text"); - KernelFunctionArguments arguments = KernelFunctionArguments.builder() + KernelArguments arguments = KernelArguments.builder() .withInput("Today is: ") .withVariable("day", "Monday") .build(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example05_InlineFunctionDefinition.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example05_InlineFunctionDefinition.java index 7ecf9a0da..52edb0dba 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example05_InlineFunctionDefinition.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example05_InlineFunctionDefinition.java @@ -11,10 +11,10 @@ import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; -import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; + import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; @@ -82,7 +82,7 @@ public static void main(String[] args) throws ConfigurationException { var result = kernel .invokeAsync(excuseFunction) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withInput("I missed the F1 final race") .build()) .block(); @@ -90,7 +90,7 @@ public static void main(String[] args) throws ConfigurationException { result = kernel.invokeAsync(excuseFunction) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withInput("sorry I forgot your birthday") .build()) .block(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example09_FunctionTypes.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example09_FunctionTypes.java index c6c1fdffa..92aab5548 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example09_FunctionTypes.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example09_FunctionTypes.java @@ -9,7 +9,6 @@ import com.azure.core.credential.KeyCredential; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; -import com.microsoft.semantickernel.aiservices.openai.textcompletion.OpenAITextGenerationService; import com.microsoft.semantickernel.contextvariables.ContextVariable; import com.microsoft.semantickernel.contextvariables.ContextVariableType; import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; @@ -17,11 +16,11 @@ import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; -import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; + import java.nio.file.Path; import java.time.Instant; import java.time.OffsetDateTime; @@ -119,7 +118,7 @@ public static void main(String[] args) throws InterruptedException { result = kernel .invokeAsync(plugin.get("InputDateTimeWithStringResult")) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable("currentDate", ContextVariable.of( @@ -135,7 +134,7 @@ public static void main(String[] args) throws InterruptedException { result = kernel.invokeAsync(plugin.get("MultipleInputsWithVoidResult")) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable("x", "x string") .withVariable("y", 100) @@ -146,7 +145,7 @@ public static void main(String[] args) throws InterruptedException { result = kernel .invokeAsync(plugin.get("ComplexInputWithStringResult")) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable( "complexObject", @@ -165,7 +164,7 @@ public String toString() { result = kernel .invokeAsync(plugin.get("InputStringTaskWithStringResult")) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable("echoInput", "return this") .build()) @@ -175,7 +174,7 @@ public String toString() { result = kernel .invokeAsync(plugin.get("InputStringTaskWithVoidResult")) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable("x", "x input") .build()) @@ -258,7 +257,7 @@ public String toString() { result = kernel.invokeAsync(plugin.get("MultipleComplexInputsWithVoidResult")) .withArguments( - KernelFunctionArguments + KernelArguments .builder() .withVariable("x", OffsetDateTime.of(1, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC)) .withVariable("y", OffsetDateTime.of(1, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC)) @@ -699,7 +698,7 @@ public Mono> WithListReturn3() { * public async Task * TaskInjectingCancellationTokenWithStringResult(CancellationToken cancellationToken) * { - * var result = $"Cancellation resquested: {cancellationToken.IsCancellationRequested}"; + * var result = $"Cancellation requested: {cancellationToken.IsCancellationRequested}"; * Console.WriteLine( * $"Running {nameof(this.TaskInjectingCultureInfoOrIFormatProviderWithStringResult)} -> Injected Cancellation Token -> result: {result}" * ); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example27_PromptFunctionsUsingChatGPT.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example27_PromptFunctionsUsingChatGPT.java index f72ec2370..1f139e2ff 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example27_PromptFunctionsUsingChatGPT.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example27_PromptFunctionsUsingChatGPT.java @@ -9,7 +9,7 @@ import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; public class Example27_PromptFunctionsUsingChatGPT { @@ -54,7 +54,7 @@ public static void main(String[] args) { var result = func.invokeAsync(kernel) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("input", "Jupiter") .build()) .withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(String.class)) diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example59_OpenAIFunctionCalling.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example59_OpenAIFunctionCalling.java index e52c2be79..d021caf88 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example59_OpenAIFunctionCalling.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example59_OpenAIFunctionCalling.java @@ -10,6 +10,7 @@ import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatMessageContent; import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIFunctionToolCall; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.functionchoice.FunctionChoiceBehavior; import com.microsoft.semantickernel.implementation.CollectionUtil; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; @@ -38,7 +39,7 @@ public class Example59_OpenAIFunctionCalling { // Only required if AZURE_CLIENT_KEY is set private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); private static final String MODEL_ID = System.getenv() - .getOrDefault("MODEL_ID", "gpt-35-turbo-2"); + .getOrDefault("MODEL_ID", "gpt-35-turbo"); // Define functions that can be called by the model public static class HelperFunctions { @@ -118,7 +119,7 @@ public static void main(String[] args) throws NoSuchMethodException { var result = kernel .invokeAsync(function) - .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withFunctionChoiceBehavior(FunctionChoiceBehavior.auto(true)) .withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(String.class)) .block(); System.out.println(result.getResult()); @@ -134,7 +135,7 @@ public static void main(String[] args) throws NoSuchMethodException { chatHistory, kernel, InvocationContext.builder() - .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(false)) + .withFunctionChoiceBehavior(FunctionChoiceBehavior.auto(false)) .withReturnMode(InvocationReturnMode.FULL_HISTORY) .build()) .block(); @@ -243,7 +244,7 @@ public static void multiTurnaroundCall() { chatHistory, kernel, InvocationContext.builder() - .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withFunctionChoiceBehavior(FunctionChoiceBehavior.auto(true)) .withReturnMode(InvocationReturnMode.FULL_HISTORY) .build()) .block(); @@ -258,7 +259,7 @@ public static void multiTurnaroundCall() { chatHistory, kernel, InvocationContext.builder() - .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withFunctionChoiceBehavior(FunctionChoiceBehavior.auto(true)) .withReturnMode(InvocationReturnMode.FULL_HISTORY) .build()) .block(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example60_AdvancedMethodFunctions.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example60_AdvancedMethodFunctions.java index 87669a40c..dfbaeb722 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example60_AdvancedMethodFunctions.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example60_AdvancedMethodFunctions.java @@ -5,7 +5,7 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.plugin.KernelPluginFactory; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import reactor.core.publisher.Mono; @@ -37,7 +37,7 @@ public static void main(String[] args) { var result = kernel .invokeAsync(FunctionsChainingPlugin.PluginName, "Function1") .withArguments( - KernelFunctionArguments + KernelArguments .builder() .build()) .withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(MyCustomType.class)) @@ -82,7 +82,7 @@ public Mono function1Async(Kernel kernel) { // Execute another function return kernel .invokeAsync(PluginName, "Function2") - .withArguments(KernelFunctionArguments.builder().build()) + .withArguments(KernelArguments.builder().build()) .withResultType(ContextVariableTypes.getGlobalVariableTypeForClass( Example60_AdvancedMethodFunctions.MyCustomType.class)) .flatMap(value -> { diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example98_GeminiFunctionCalling.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example98_GeminiFunctionCalling.java index 0dd425f6e..b585775db 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example98_GeminiFunctionCalling.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/functions/Example98_GeminiFunctionCalling.java @@ -13,7 +13,7 @@ import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.orchestration.ToolCallBehavior; import com.microsoft.semantickernel.plugin.KernelPluginFactory; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; @@ -140,7 +140,7 @@ public static void main(String[] args) throws NoSuchMethodException { var fn = kernel.getFunction(geminiFunction.getPluginName(), geminiFunction.getFunctionName()); - var arguments = KernelFunctionArguments.builder(); + var arguments = KernelArguments.builder(); geminiFunction.getFunctionCall().getArgs().getFieldsMap() .forEach((key, value) -> { arguments.withVariable(key, value.getStringValue()); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/CustomTypes_Example.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/CustomTypes_Example.java index e809474dc..fcf2cc812 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/CustomTypes_Example.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/CustomTypes_Example.java @@ -13,7 +13,7 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.contextvariables.converters.ContextVariableJacksonConverter; import com.microsoft.semantickernel.exceptions.ConfigurationException; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import java.io.IOException; import java.util.Arrays; @@ -114,7 +114,7 @@ private static void exampleBuildingCustomConverter( Pet updated = kernel.invokePromptAsync( "Change Sandy's name to Daisy:\n{{$Sandy}}", - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("Sandy", sandy, typeConverter) .build()) .withTypeConverter(typeConverter) @@ -139,7 +139,7 @@ public static void exampleUsingJackson(ChatCompletionService chatCompletionServi // Invoke the prompt with the custom converter Pet updated = kernel.invokePromptAsync( "Increase Sandy's age by a year:\n{{$Sandy}}", - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("Sandy", sandy, typeConverter) .build()) .withTypeConverter(typeConverter) @@ -167,7 +167,7 @@ public static void exampleUsingGlobalTypes(ChatCompletionService chatCompletionS // No need to explicitly tell the invocation how to convert the type Pet updated = kernel.invokePromptAsync( "Sandy's is actually a cat correct this:\n{{$Sandy}}", - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("Sandy", sandy) .build()) .withResultType(Pet.class) diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionTelemetry_Example.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionTelemetry_Example.java new file mode 100644 index 000000000..4ca35a80f --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionTelemetry_Example.java @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.java; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.exceptions.ConfigurationException; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.InvocationReturnMode; +import com.microsoft.semantickernel.orchestration.ToolCallBehavior; +import com.microsoft.semantickernel.plugin.KernelPluginFactory; +import com.microsoft.semantickernel.samples.syntaxexamples.functions.Example59_OpenAIFunctionCalling.PetPlugin; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; +import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; +import com.microsoft.semantickernel.services.ServiceNotFoundException; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import reactor.core.publisher.Mono; + +public class FunctionTelemetry_Example { + /* + * // Get the Application Insights agent from + * https://github.com/microsoft/ApplicationInsights-Java, e.g: + * ``` + * wget -O "/tmp/applicationinsights-agent-3.6.1.jar" + * "https://github.com/microsoft/ApplicationInsights-Java/releases/download/3.6.1/applicationinsights-agent-3.6.1.jar" + * ``` + * + * // Get your application insights connection string from the Azure portal + * ``` + * CLIENT_ENDPOINT="" \ + * AZURE_CLIENT_KEY="" \ + * APPLICATIONINSIGHTS_CONNECTION_STRING="" \ + * MAVEN_OPTS="-javaagent:/tmp/applicationinsights-agent-3.6.1.jar" \ + * ../../../mvnw package exec:java -Dsample="java.FunctionTelemetry_Example" + * ``` + * + * If you open the Application Insights "Live metrics" view while running this example, you + * should see the telemetry in real-time. + * Otherwise within a few minutes, you should see the telemetry in the Application Insights -> + * Investigate -> Transaction search ui in the Azure portal. + */ + + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = "gpt-4o"; + + public static void main(String[] args) + throws ConfigurationException, IOException, NoSuchMethodException, InterruptedException { + requestsWithSpanContext(); + testNestedCalls(); + requestsWithScope(); + + Thread.sleep(1000); + } + + private static void requestsWithSpanContext() throws IOException { + Span fakeRequest = GlobalOpenTelemetry.getTracer("Custom") + .spanBuilder("GET /requestsWithSpanContext") + .setSpanKind(SpanKind.SERVER) + .setAttribute("http.request.method", "GET") + .setAttribute("url.path", "/requestsWithSpanContext") + .setAttribute("url.scheme", "http") + .startSpan(); + + // Pass span context to the telemetry object to correlate telemetry with the request + SemanticKernelTelemetry telemetry = new SemanticKernelTelemetry( + GlobalOpenTelemetry.getTracer("Custom"), + fakeRequest.getSpanContext()); + + sequentialFunctionCalls(telemetry); + + fakeRequest.setStatus(StatusCode.OK); + fakeRequest.end(); + } + + private static void requestsWithScope() throws IOException { + Span fakeRequest = GlobalOpenTelemetry.getTracer("Custom") + .spanBuilder("GET /requestsWithScope") + .setSpanKind(SpanKind.SERVER) + .setAttribute("http.request.method", "GET") + .setAttribute("url.path", "/requestsWithScope") + .setAttribute("url.scheme", "http") + .startSpan(); + + // Pass span context to the telemetry object to correlate telemetry with the request + SemanticKernelTelemetry telemetry = new SemanticKernelTelemetry(); + + try (Scope scope = fakeRequest.makeCurrent()) { + sequentialFunctionCalls(telemetry); + } + + fakeRequest.setStatus(StatusCode.OK); + fakeRequest.end(); + } + + public static void sequentialFunctionCalls(SemanticKernelTelemetry telemetry) { + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + ChatCompletionService chat = OpenAIChatCompletion.builder() + .withModelId(MODEL_ID) + .withOpenAIAsyncClient(client) + .build(); + + var plugin = KernelPluginFactory.createFromObject(new PetPlugin(), "PetPlugin"); + + var kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chat) + .withPlugin(plugin) + .build(); + + var chatHistory = new ChatHistory(); + chatHistory.addUserMessage( + "What is the name and type of the pet with id ca2fc6bc-1307-4da6-a009-d7bf88dec37b?"); + + var messages = chat.getChatMessageContentsAsync( + chatHistory, + kernel, + InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.FULL_HISTORY) + .withTelemetry(telemetry) + .build()) + .block(); + + chatHistory = new ChatHistory(messages); + + System.out.println( + "THE NAME AND TYPE IS: " + chatHistory.getLastMessage().get().getContent()); + } + + public static void testNestedCalls() { + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + ChatCompletionService chat = OpenAIChatCompletion.builder() + .withModelId(MODEL_ID) + .withOpenAIAsyncClient(client) + .build(); + + var plugin = KernelPluginFactory.createFromObject(new TextAnalysisPlugin(), + "TextAnalysisPlugin"); + + var kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chat) + .withPlugin(plugin) + .build(); + + SemanticKernelTelemetry telemetry = new SemanticKernelTelemetry(); + + Span span = GlobalOpenTelemetry.getTracer("Test") + .spanBuilder("testNestedCalls span") + .setSpanKind(SpanKind.SERVER) + .startSpan(); + + try (Scope scope = span.makeCurrent()) { + String analysed = kernel + .invokePromptAsync( + """ + Analyse the following text: + Hello There + """, + KernelArguments.builder().build(), + InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY) + .withTelemetry(telemetry) + .build()) + .withResultType(String.class) + .map(result -> { + return result.getResult(); + }) + .block(); + System.out.println(analysed); + } finally { + span.end(); + } + + } + + public static class TextAnalysisPlugin { + + @DefineKernelFunction(description = "Change all string chars to uppercase.", name = "Uppercase") + public String uppercase( + @KernelFunctionParameter(description = "Text to uppercase", name = "input") String text) { + return text.toUpperCase(Locale.ROOT); + } + + @DefineKernelFunction(name = "sha256sum", description = "Calculates a sha256 of the input", returnType = "string") + public Mono sha256sum( + @KernelFunctionParameter(name = "input", description = "The input to checksum", type = String.class) String input, + Kernel kernel, + SemanticKernelTelemetry telemetry) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + String hashStr = new BigInteger(1, hash).toString(16); + + return kernel + .invokePromptAsync( + """ + Uppercase the following text: + === BEGIN TEXT === + %s + === END TEXT === + """.formatted(hashStr) + .stripIndent(), + null, + InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY) + .withTelemetry(telemetry) + .build()) + .withResultType(String.class) + .map(result -> { + return result.getResult(); + }); + } + + @DefineKernelFunction(name = "formatAnswer", description = "Formats an answer", returnType = "string") + public Mono formatAnswer( + @KernelFunctionParameter(name = "input", description = "The input to format", type = String.class) String input, + Kernel kernel, + SemanticKernelTelemetry telemetry) throws ServiceNotFoundException { + + return kernel + .invokePromptAsync( + """ + Translate the following text into Italian: + === BEGIN TEXT === + %s + === END TEXT === + """.formatted(input) + .stripIndent()) + .withResultType(String.class) + .map(result -> { + return result.getResult(); + }); + } + + @DefineKernelFunction(name = "analyseInput", description = "Gives a text analysis of the input", returnType = "string") + public Mono analyseInput( + @KernelFunctionParameter(name = "input", description = "The input to analyse", type = String.class) String input, + Kernel kernel, + SemanticKernelTelemetry telemetry) throws ServiceNotFoundException { + + return kernel + .invokePromptAsync( + """ + Calculating sha256sum of the following text: + === BEGIN TEXT === + %s + === END TEXT === + """.formatted(input) + .stripIndent(), + null, + InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY) + .withTelemetry(telemetry) + .build()) + .withResultType(String.class) + .map(result -> { + return result.getResult(); + }) + .flatMap(answer -> { + return kernel + .invokePromptAsync( + """ + Format the following text: + === BEGIN TEXT === + %s + === END TEXT === + """.formatted(answer) + .stripIndent()) + .withInvocationContext( + InvocationContext.builder() + .withToolCallBehavior( + ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY) + .withTelemetry(telemetry) + .build()) + .withArguments(null) + .withTelemetry(telemetry) + .withResultType(String.class); + }) + .map(it -> { + return it.getResult(); + }); + } + + } + +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionsHandlebars_Example.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionsHandlebars_Example.java index b13f06998..29ad37696 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionsHandlebars_Example.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionsHandlebars_Example.java @@ -4,7 +4,7 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.exceptions.ConfigurationException; import com.microsoft.semantickernel.plugin.KernelPluginFactory; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelPromptTemplateFactory; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; @@ -41,7 +41,7 @@ public static void main(String[] args) throws ConfigurationException, IOExceptio var renderedPrompt = promptTemplate.renderAsync( kernel, - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("choices", choices) .build(), null) diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionsWithinPrompts_Example.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionsWithinPrompts_Example.java index ceb9983ed..7e92e3e6e 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionsWithinPrompts_Example.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionsWithinPrompts_Example.java @@ -12,7 +12,7 @@ import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.ConversationSummaryPlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; @@ -154,7 +154,7 @@ public static void main(String[] args) throws ConfigurationException, IOExceptio // Invoke handlebars prompt var intent = kernel.invokeAsync(getIntent) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("request", request) .withVariable("choices", choices) .withVariable("history", historyString) @@ -173,7 +173,7 @@ public static void main(String[] args) throws ConfigurationException, IOExceptio // Get chat response var chatResult = kernel.invokeAsync(chat) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("request", request) .withVariable("history", historyString) .build()) diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/KernelFunctionYaml_Example.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/KernelFunctionYaml_Example.java index 900513c84..10e9a014e 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/KernelFunctionYaml_Example.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/KernelFunctionYaml_Example.java @@ -10,13 +10,14 @@ import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; import com.microsoft.semantickernel.exceptions.ConfigurationException; import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionYaml; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; -import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; import java.io.IOException; +import javax.annotation.Nullable; public class KernelFunctionYaml_Example { @@ -29,7 +30,10 @@ public class KernelFunctionYaml_Example { .getOrDefault("MODEL_ID", "gpt-35-turbo"); public static void main(String[] args) throws ConfigurationException, IOException { + run(null); + } + public static void run(@Nullable SemanticKernelTelemetry telemetry) throws IOException { OpenAIAsyncClient client; if (AZURE_CLIENT_KEY != null) { @@ -51,12 +55,13 @@ public static void main(String[] args) throws ConfigurationException, IOExceptio Builder kernelBuilder = Kernel.builder() .withAIService(ChatCompletionService.class, openAIChatCompletion); - semanticKernelTemplate(kernelBuilder.build()); - handlebarsTemplate(kernelBuilder.build()); - + semanticKernelTemplate(kernelBuilder.build(), telemetry); + handlebarsTemplate(kernelBuilder.build(), telemetry); } - private static void handlebarsTemplate(Kernel kernel) throws IOException { + private static void handlebarsTemplate(Kernel kernel, + @Nullable SemanticKernelTelemetry telemetry) + throws IOException { String yaml = EmbeddedResourceLoader.readFile("GenerateStoryHandlebars.yaml", KernelFunctionYaml_Example.class); @@ -65,16 +70,19 @@ private static void handlebarsTemplate(Kernel kernel) throws IOException { FunctionResult result = function .invokeAsync(kernel) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("length", 5) .withVariable("topic", "dogs") .build()) + .withTelemetry(telemetry) .block(); System.out.println(result.getResult()); } - private static void semanticKernelTemplate(Kernel kernel) throws IOException { + private static void semanticKernelTemplate(Kernel kernel, + @Nullable SemanticKernelTelemetry telemetry) + throws IOException { String yaml = EmbeddedResourceLoader.readFile("GenerateStory.yaml", KernelFunctionYaml_Example.class); @@ -83,10 +91,11 @@ private static void semanticKernelTemplate(Kernel kernel) throws IOException { FunctionResult result = function .invokeAsync(kernel) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("length", 5) .withVariable("topic", "cats") .build()) + .withTelemetry(telemetry) .block(); System.out.println(result.getResult()); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/InMemory_DataStorage.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/InMemoryVolatileVectorStore.java similarity index 68% rename from samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/InMemory_DataStorage.java rename to samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/InMemoryVolatileVectorStore.java index d92c1e067..ef4fe9549 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/InMemory_DataStorage.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/InMemoryVolatileVectorStore.java @@ -5,32 +5,26 @@ import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.credential.KeyCredential; -import com.azure.core.util.ClientOptions; -import com.azure.core.util.MetricsOptions; -import com.azure.core.util.TracingOptions; -import com.azure.search.documents.indexes.SearchIndexAsyncClient; -import com.azure.search.documents.indexes.SearchIndexClientBuilder; import com.microsoft.semantickernel.aiservices.openai.textembedding.OpenAITextEmbeddingGenerationService; -import com.microsoft.semantickernel.connectors.data.azureaisearch.AzureAISearchVectorStore; -import com.microsoft.semantickernel.connectors.data.azureaisearch.AzureAISearchVectorStoreOptions; -import com.microsoft.semantickernel.connectors.data.azureaisearch.AzureAISearchVectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; import com.microsoft.semantickernel.data.VolatileVectorStore; -import com.microsoft.semantickernel.data.VolatileVectorStoreRecordCollection; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordDataAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordKeyAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordVectorAttribute; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.nio.charset.StandardCharsets; +import com.microsoft.semantickernel.data.VolatileVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class InMemoryVolatileVectorStore { -public class InMemory_DataStorage { private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); @@ -43,13 +37,13 @@ public class InMemory_DataStorage { private static final int EMBEDDING_DIMENSIONS = 1536; static class GitHubFile { - @VectorStoreRecordKeyAttribute() + @VectorStoreRecordKey private final String id; - @VectorStoreRecordDataAttribute(hasEmbedding = true, embeddingFieldName = "embedding") + @VectorStoreRecordData private final String description; - @VectorStoreRecordDataAttribute + @VectorStoreRecordData private final String link; - @VectorStoreRecordVectorAttribute(dimensions = EMBEDDING_DIMENSIONS, indexKind = "Hnsw") + @VectorStoreRecordVector(dimensions = EMBEDDING_DIMENSIONS, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.COSINE_DISTANCE) private final List embedding; public GitHubFile( @@ -71,9 +65,16 @@ public String getDescription() { return description; } + public String getLink() { + return link; + } + + public List getEmbedding() { + return embedding; + } + static String encodeId(String realId) { - byte[] bytes = Base64.getUrlEncoder().encode(realId.getBytes(StandardCharsets.UTF_8)); - return new String(bytes, StandardCharsets.UTF_8); + return VectorStoreWithAzureAISearch.GitHubFile.encodeId(realId); } } @@ -102,31 +103,51 @@ public static void main(String[] args) { .withDimensions(EMBEDDING_DIMENSIONS) .build(); - inMemoryDataStorage(embeddingGeneration); + inMemoryStoreAndSearch(embeddingGeneration); } - public static void inMemoryDataStorage( + public static void inMemoryStoreAndSearch( OpenAITextEmbeddingGenerationService embeddingGeneration) { // Create a new Volatile vector store var volatileVectorStore = new VolatileVectorStore(); String collectionName = "skgithubfiles"; - var collection = volatileVectorStore.getCollection(collectionName, GitHubFile.class, null); + var collection = volatileVectorStore.getCollection(collectionName, + VolatileVectorStoreRecordCollectionOptions.builder() + .withRecordClass(GitHubFile.class) + .build()); // Create collection if it does not exist and store data - List ids = collection + collection .createCollectionIfNotExistsAsync() .then(storeData(collection, embeddingGeneration, sampleData())) .block(); - // Retrieve all records from the collection - List data = collection.getBatchAsync(ids, null).block(); + // Search for results + // Volatile store executes an exhaustive search, for approximate search use Azure AI Search, Redis or JDBC with PostgreSQL + var results = search("How to get started", collection, embeddingGeneration).block(); - data.forEach(gitHubFile -> System.out.println("Retrieved: " + gitHubFile.getDescription())); + if (results == null || results.getTotalCount() == 0) { + System.out.println("No search results found."); + return; + } + var searchResult = results.getResults().get(0); + System.out.printf("Search result with score: %f.%n Link: %s, Description: %s%n", + searchResult.getScore(), searchResult.getRecord().link, + searchResult.getRecord().description); + } + + private static Mono> search( + String searchText, + VectorStoreRecordCollection recordCollection, + OpenAITextEmbeddingGenerationService embeddingGeneration) { + // Generate embeddings for the search text and search for the closest records + return embeddingGeneration.generateEmbeddingAsync(searchText) + .flatMap(r -> recordCollection.searchAsync(r.getVector(), null)); } private static Mono> storeData( - VolatileVectorStoreRecordCollection recordCollection, + VectorStoreRecordCollection recordCollection, OpenAITextEmbeddingGenerationService embeddingGeneration, Map data) { @@ -134,6 +155,7 @@ private static Mono> storeData( .flatMap(entry -> { System.out.println("Save '" + entry.getKey() + "' to memory."); + // Generate embeddings for the data and store it return embeddingGeneration .generateEmbeddingsAsync(Collections.singletonList(entry.getValue())) .flatMap(embeddings -> { diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/JDBC_DataStorage.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/JDBC_DataStorage.java deleted file mode 100644 index 2379e572c..000000000 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/JDBC_DataStorage.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.microsoft.semantickernel.samples.syntaxexamples.memory; - -import com.azure.ai.openai.OpenAIAsyncClient; -import com.azure.ai.openai.OpenAIClientBuilder; -import com.azure.core.credential.AzureKeyCredential; -import com.azure.core.credential.KeyCredential; -import com.microsoft.semantickernel.aiservices.openai.textembedding.OpenAITextEmbeddingGenerationService; -import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStore; -import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreOptions; -import com.microsoft.semantickernel.connectors.data.jdbc.MySQLVectorStoreQueryProvider; -import com.microsoft.semantickernel.data.VectorStoreRecordCollection; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordDataAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordKeyAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordVectorAttribute; -import com.mysql.cj.jdbc.MysqlDataSource; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import javax.sql.DataSource; -import java.nio.charset.StandardCharsets; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class JDBC_DataStorage { - - private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); - private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); - - // Only required if AZURE_CLIENT_KEY is set - private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); - private static final String MODEL_ID = System.getenv() - .getOrDefault("EMBEDDING_MODEL_ID", "text-embedding-3-large"); - private static final int EMBEDDING_DIMENSIONS = 1536; - - // Run a MySQL server with: - // docker run -d --name mysql-container -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=sk -p 3306:3306 mysql:latest - - static class GitHubFile { - @VectorStoreRecordKeyAttribute() - private final String id; - @VectorStoreRecordDataAttribute(hasEmbedding = true, embeddingFieldName = "embedding") - private final String description; - @VectorStoreRecordDataAttribute - private final String link; - @VectorStoreRecordVectorAttribute(dimensions = EMBEDDING_DIMENSIONS, indexKind = "Hnsw") - private final List embedding; - - public GitHubFile() { - this(null, null, null, Collections.emptyList()); - } - - public GitHubFile( - String id, - String description, - String link, - List embedding) { - this.id = id; - this.description = description; - this.link = link; - this.embedding = embedding; - } - - public String getId() { - return id; - } - - public String getDescription() { - return description; - } - - static String encodeId(String realId) { - byte[] bytes = Base64.getUrlEncoder().encode(realId.getBytes(StandardCharsets.UTF_8)); - return new String(bytes, StandardCharsets.UTF_8); - } - } - - public static void main(String[] args) throws SQLException { - System.out.println("=============================================================="); - System.out.println("========== JDBC Vector Store Example =============="); - System.out.println("=============================================================="); - - OpenAIAsyncClient client; - - if (AZURE_CLIENT_KEY != null) { - client = new OpenAIClientBuilder() - .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) - .endpoint(CLIENT_ENDPOINT) - .buildAsyncClient(); - - } else { - client = new OpenAIClientBuilder() - .credential(new KeyCredential(CLIENT_KEY)) - .buildAsyncClient(); - } - - var embeddingGeneration = OpenAITextEmbeddingGenerationService.builder() - .withOpenAIAsyncClient(client) - .withModelId(MODEL_ID) - .withDimensions(EMBEDDING_DIMENSIONS) - .build(); - - var dataSource = new MysqlDataSource(); - dataSource.setUrl("jdbc:mysql://localhost:3306/sk"); - dataSource.setPassword("root"); - dataSource.setUser("root"); - - dataStorageWithMySQL(dataSource, embeddingGeneration); - } - - public static void dataStorageWithMySQL( - DataSource dataSource, - OpenAITextEmbeddingGenerationService embeddingGeneration) { - - // Build a query provider - var queryProvider = MySQLVectorStoreQueryProvider.builder() - .withDataSource(dataSource) - .build(); - - // Create a new vector store - var jdbcVectorStore = JDBCVectorStore.builder() - .withDataSource(dataSource) - .withOptions(JDBCVectorStoreOptions.builder() - .withQueryProvider(queryProvider) - .build()) - .build(); - - String collectionName = "skgithubfiles"; - var collection = jdbcVectorStore.getCollection(collectionName, GitHubFile.class, - null); - - // Create collection if it does not exist and store data - List ids = collection - .createCollectionIfNotExistsAsync() - .then(storeData(collection, embeddingGeneration, sampleData())) - .block(); - - List data = collection.getBatchAsync(ids, null).block(); - - data.forEach(gitHubFile -> System.out.println("Retrieved: " + gitHubFile.getDescription())); - } - - private static Mono> storeData( - VectorStoreRecordCollection recordStore, - OpenAITextEmbeddingGenerationService embeddingGeneration, - Map data) { - - return Flux.fromIterable(data.entrySet()) - .flatMap(entry -> { - System.out.println("Save '" + entry.getKey() + "' to memory."); - - return embeddingGeneration - .generateEmbeddingsAsync(Collections.singletonList(entry.getValue())) - .flatMap(embeddings -> { - GitHubFile gitHubFile = new GitHubFile( - GitHubFile.encodeId(entry.getKey()), - entry.getValue(), - entry.getKey(), - embeddings.get(0).getVector()); - return recordStore.upsertAsync(gitHubFile, null); - }); - }) - .collectList(); - } - - private static Map sampleData() { - return Arrays.stream(new String[][] { - { "https://github.com/microsoft/semantic-kernel/blob/main/README.md", - "README: Installation, getting started with Semantic Kernel, 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/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/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/AzureAISearch_DataStorage.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithAzureAISearch.java similarity index 70% rename from samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/AzureAISearch_DataStorage.java rename to samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithAzureAISearch.java index 92316385a..382bf0f83 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/AzureAISearch_DataStorage.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithAzureAISearch.java @@ -11,15 +11,15 @@ import com.azure.search.documents.indexes.SearchIndexAsyncClient; import com.azure.search.documents.indexes.SearchIndexClientBuilder; import com.microsoft.semantickernel.aiservices.openai.textembedding.OpenAITextEmbeddingGenerationService; -import com.microsoft.semantickernel.connectors.data.azureaisearch.AzureAISearchVectorStore; -import com.microsoft.semantickernel.connectors.data.azureaisearch.AzureAISearchVectorStoreOptions; -import com.microsoft.semantickernel.connectors.data.azureaisearch.AzureAISearchVectorStoreRecordCollection; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordDataAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordKeyAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordVectorAttribute; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - +import com.microsoft.semantickernel.data.azureaisearch.AzureAISearchVectorStore; +import com.microsoft.semantickernel.data.azureaisearch.AzureAISearchVectorStoreOptions; +import com.microsoft.semantickernel.data.azureaisearch.AzureAISearchVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; @@ -28,7 +28,12 @@ import java.util.Map; import java.util.stream.Collectors; -public class AzureAISearch_DataStorage { +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class VectorStoreWithAzureAISearch { + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); @@ -45,13 +50,13 @@ public class AzureAISearch_DataStorage { private static final int EMBEDDING_DIMENSIONS = 1536; static class GitHubFile { - @VectorStoreRecordKeyAttribute() + @VectorStoreRecordKey private final String id; - @VectorStoreRecordDataAttribute(hasEmbedding = true, embeddingFieldName = "embedding") + @VectorStoreRecordData private final String description; - @VectorStoreRecordDataAttribute + @VectorStoreRecordData private final String link; - @VectorStoreRecordVectorAttribute(dimensions = EMBEDDING_DIMENSIONS, indexKind = "Hnsw") + @VectorStoreRecordVector(dimensions = EMBEDDING_DIMENSIONS, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.COSINE_SIMILARITY) private final List embedding; public GitHubFile() { @@ -106,22 +111,26 @@ public static void main(String[] args) { .clientOptions(clientOptions()) .buildAsyncClient(); - dataStorageWithAzureAISearch(searchClient, embeddingGeneration); + storeAndSearch(searchClient, embeddingGeneration); } - public static void dataStorageWithAzureAISearch( + public static void storeAndSearch( SearchIndexAsyncClient searchClient, OpenAITextEmbeddingGenerationService embeddingGeneration) { - // Create a new Azure AI Search vector store + // Build an Azure AI Search Vector Store var azureAISearchVectorStore = AzureAISearchVectorStore.builder() - .withClient(searchClient) + .withSearchIndexAsyncClient(searchClient) .withOptions(new AzureAISearchVectorStoreOptions()) .build(); String collectionName = "skgithubfiles"; - var collection = azureAISearchVectorStore.getCollection(collectionName, GitHubFile.class, - null); + var collection = azureAISearchVectorStore + .getCollection( + collectionName, + AzureAISearchVectorStoreRecordCollectionOptions.builder() + .withRecordClass(GitHubFile.class) + .build()); // Create collection if it does not exist and store data collection @@ -129,18 +138,31 @@ public static void dataStorageWithAzureAISearch( .then(storeData(collection, embeddingGeneration, sampleData())) .block(); - // Query the Azure AI Search client for results - // This might take a few seconds to return the best result - var result = searchClient.getSearchAsyncClient(collectionName) - .search("How to get started with the Semantic Kernel?") - .blockFirst(); + // Search for results + // Might need to wait for the data to be indexed + var results = search("How to get started", collection, embeddingGeneration).block(); - GitHubFile gitHubFile = result.getDocument(GitHubFile.class); - System.out.println("Best result: " + gitHubFile.description + ". Link: " + gitHubFile.link); + if (results == null || results.getTotalCount() == 0) { + System.out.println("No search results found."); + return; + } + var searchResult = results.getResults().get(0); + System.out.printf("Search result with score: %f.%n Link: %s, Description: %s%n", + searchResult.getScore(), searchResult.getRecord().link, + searchResult.getRecord().description); + } + + private static Mono> search( + String searchText, + VectorStoreRecordCollection recordCollection, + OpenAITextEmbeddingGenerationService embeddingGeneration) { + // Generate embeddings for the search text and search for the closest records + return embeddingGeneration.generateEmbeddingAsync(searchText) + .flatMap(r -> recordCollection.searchAsync(r.getVector(), null)); } private static Mono> storeData( - AzureAISearchVectorStoreRecordCollection recordStore, + VectorStoreRecordCollection recordCollection, OpenAITextEmbeddingGenerationService embeddingGeneration, Map data) { @@ -148,6 +170,7 @@ private static Mono> storeData( .flatMap(entry -> { System.out.println("Save '" + entry.getKey() + "' to memory."); + // Generate embeddings for the data and store it return embeddingGeneration .generateEmbeddingsAsync(Collections.singletonList(entry.getValue())) .flatMap(embeddings -> { @@ -156,7 +179,7 @@ private static Mono> storeData( entry.getValue(), entry.getKey(), embeddings.get(0).getVector()); - return recordStore.upsertAsync(gitHubFile, null); + return recordCollection.upsertAsync(gitHubFile, null); }); }) .collectList(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithJDBC.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithJDBC.java new file mode 100644 index 000000000..b9ff7aa91 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithJDBC.java @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.memory; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.microsoft.semantickernel.aiservices.openai.textembedding.OpenAITextEmbeddingGenerationService; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; + +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.postgresql.ds.PGSimpleDataSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class VectorStoreWithJDBC { + + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = System.getenv() + .getOrDefault("EMBEDDING_MODEL_ID", "text-embedding-3-large"); + private static final int EMBEDDING_DIMENSIONS = 1536; + + static class GitHubFile { + @VectorStoreRecordKey + private final String id; + @VectorStoreRecordData + private final String description; + @VectorStoreRecordData + private final String link; + @VectorStoreRecordVector(dimensions = EMBEDDING_DIMENSIONS, distanceFunction = DistanceFunction.COSINE_DISTANCE) + private final List embedding; + + public GitHubFile() { + this(null, null, null, Collections.emptyList()); + } + + public GitHubFile( + String id, + String description, + String link, + List embedding) { + this.id = id; + this.description = description; + this.link = link; + this.embedding = embedding; + } + + public String getId() { + return id; + } + + public String getDescription() { + return description; + } + + public String getLink() { + return link; + } + + public List getEmbedding() { + return embedding; + } + + static String encodeId(String realId) { + byte[] bytes = Base64.getUrlEncoder().encode(realId.getBytes(StandardCharsets.UTF_8)); + return new String(bytes, StandardCharsets.UTF_8); + } + } + + // Run a PostgreSQL server with: + // docker run -d --name pgvector-container -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=root -e POSTGRES_DB=sk -p 5432:5432 pgvector/pgvector:pg17 + + public static void main(String[] args) throws SQLException { + System.out.println("=============================================================="); + System.out.println("=============== JDBC Vector Store Example ===================="); + System.out.println("=============================================================="); + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + // Create an OpenAI text embedding generation service + var embeddingGeneration = OpenAITextEmbeddingGenerationService.builder() + .withOpenAIAsyncClient(client) + .withModelId(MODEL_ID) + .withDimensions(EMBEDDING_DIMENSIONS) + .build(); + + storeAndSearch(embeddingGeneration); + } + + public static void storeAndSearch(OpenAITextEmbeddingGenerationService embeddingGeneration) { + // Configure the data source + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl("jdbc:postgresql://localhost:5432/sk"); + dataSource.setUser("postgres"); + dataSource.setPassword("root"); + + // Build a query provider + // Other available query providers are PostgreSQLVectorStoreQueryProvider and SQLiteVectorStoreQueryProvider + var queryProvider = PostgreSQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + + // Build a vector store + var jdbcVectorStore = JDBCVectorStore.builder() + .withDataSource(dataSource) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + // Set up the record collection to use + String collectionName = "skgithubfiles"; + var collection = jdbcVectorStore.getCollection(collectionName, + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(GitHubFile.class) + .build()); + + // Create collection if it does not exist and store data + collection + .createCollectionIfNotExistsAsync() + .then(storeData(collection, embeddingGeneration, sampleData())) + .block(); + + // Search for results + var results = search("How to get started", collection, embeddingGeneration).block(); + + if (results == null || results.getTotalCount() == 0) { + System.out.println("No search results found."); + return; + } + var searchResult = results.getResults().get(0); + System.out.printf("Search result with score: %f.%n Link: %s, Description: %s%n", + searchResult.getScore(), searchResult.getRecord().link, + searchResult.getRecord().description); + } + + private static Mono> search( + String searchText, + VectorStoreRecordCollection recordCollection, + OpenAITextEmbeddingGenerationService embeddingGeneration) { + // Generate embeddings for the search text and search for the closest records + return embeddingGeneration.generateEmbeddingAsync(searchText) + .flatMap(r -> recordCollection.searchAsync(r.getVector(), null)); + } + + private static Mono> storeData( + VectorStoreRecordCollection recordStore, + OpenAITextEmbeddingGenerationService embeddingGeneration, + Map data) { + + return Flux.fromIterable(data.entrySet()) + .flatMap(entry -> { + System.out.println("Save '" + entry.getKey() + "' to memory."); + + // Generate embeddings for the data and store it + return embeddingGeneration + .generateEmbeddingsAsync(Collections.singletonList(entry.getValue())) + .flatMap(embeddings -> { + GitHubFile gitHubFile = new GitHubFile( + GitHubFile.encodeId(entry.getKey()), + entry.getValue(), + entry.getKey(), + embeddings.get(0).getVector()); + return recordStore.upsertAsync(gitHubFile, null); + }); + }) + .collectList(); + } + + private static Map sampleData() { + return Arrays.stream(new String[][] { + { "https://github.com/microsoft/semantic-kernel/blob/main/README.md", + "README: Installation, getting started with Semantic Kernel, 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/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/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithOracle.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithOracle.java new file mode 100644 index 000000000..e32131050 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithOracle.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.memory; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollection; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.oracle.OracleVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.samples.documentationexamples.data.index.Hotel; +import java.sql.SQLException; +import java.util.Collections; +import oracle.jdbc.datasource.impl.OracleDataSource; + +public class VectorStoreWithOracle { + + public static void main(String[] args) throws SQLException { + System.out.println("=============================================================="); + System.out.println("============== Oracle Vector Store Example ==================="); + System.out.println("=============================================================="); + + // Configure the data source + OracleDataSource dataSource = new OracleDataSource(); + dataSource.setURL("jdbc:oracle:thin:@localhost:1521/FREEPDB1"); + dataSource.setUser("scott"); + dataSource.setPassword("tiger"); + + // Build a query provider + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + + // Build a vector store + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(dataSource) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + // Get a collection from the vector store + VectorStoreRecordCollection collection = vectorStore.getCollection( + "skhotels", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + + // Create the collection if it doesn't exist yet. + collection.createCollectionAsync().block(); + + collection.upsertAsync(new Hotel("1", + "HotelOne", + "Desc for HotelOne", + Collections.emptyList(), Collections.emptyList()), + null) + .block(); + + } + +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/Redis_DataStorage.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithRedis.java similarity index 65% rename from samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/Redis_DataStorage.java rename to samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithRedis.java index cde31a3ae..b6bebff81 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/Redis_DataStorage.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/memory/VectorStoreWithRedis.java @@ -5,28 +5,30 @@ import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.credential.KeyCredential; -import com.azure.core.util.ClientOptions; -import com.azure.core.util.MetricsOptions; -import com.azure.core.util.TracingOptions; import com.microsoft.semantickernel.aiservices.openai.textembedding.OpenAITextEmbeddingGenerationService; -import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStore; -import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStoreOptions; -import com.microsoft.semantickernel.data.VectorStoreRecordCollection; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordDataAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordKeyAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordVectorAttribute; -import java.nio.charset.StandardCharsets; +import com.microsoft.semantickernel.data.redis.RedisJsonVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.redis.RedisStorageType; +import com.microsoft.semantickernel.data.redis.RedisVectorStore; +import com.microsoft.semantickernel.data.redis.RedisVectorStoreOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; + import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; + +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import redis.clients.jedis.JedisPooled; -public class Redis_DataStorage { +public class VectorStoreWithRedis { private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); @@ -38,19 +40,14 @@ public class Redis_DataStorage { .getOrDefault("EMBEDDING_MODEL_ID", "text-embedding-3-large"); private static final int EMBEDDING_DIMENSIONS = 1536; - // Can start a test server with: - // docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest - private static final String REDIS_URL = "redis://127.0.0.1:6379"; - public static class GitHubFile { - - @VectorStoreRecordKeyAttribute() + @VectorStoreRecordKey private final String id; - @VectorStoreRecordDataAttribute(hasEmbedding = true, embeddingFieldName = "embedding") + @VectorStoreRecordData private final String description; - @VectorStoreRecordDataAttribute + @VectorStoreRecordData private final String link; - @VectorStoreRecordVectorAttribute(dimensions = EMBEDDING_DIMENSIONS, indexKind = "Hnsw") + @VectorStoreRecordVector(dimensions = EMBEDDING_DIMENSIONS, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.COSINE_DISTANCE) private final List embedding; public GitHubFile() { @@ -76,15 +73,26 @@ public String getDescription() { return description; } + public String getLink() { + return link; + } + + public List getEmbedding() { + return embedding; + } + static String encodeId(String realId) { - byte[] bytes = Base64.getUrlEncoder().encode(realId.getBytes(StandardCharsets.UTF_8)); - return new String(bytes, StandardCharsets.UTF_8); + return VectorStoreWithAzureAISearch.GitHubFile.encodeId(realId); } } + // Can start a test server with: + // docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest + private static final String REDIS_URL = "redis://127.0.0.1:6379"; + public static void main(String[] args) { System.out.println("=============================================================="); - System.out.println("========== Redis Vector Store Example =============="); + System.out.println("================ Redis Vector Store Example =================="); System.out.println("=============================================================="); OpenAIAsyncClient client; @@ -107,34 +115,56 @@ public static void main(String[] args) { .withDimensions(EMBEDDING_DIMENSIONS) .build(); - dataStorageWithRedis(embeddingGeneration); + storeAndSearch(embeddingGeneration); } - public static void dataStorageWithRedis( + public static void storeAndSearch( OpenAITextEmbeddingGenerationService embeddingGeneration) { - + // Configure redis client JedisPooled jedis = new JedisPooled(REDIS_URL); - RedisVectorStore vectorStore = RedisVectorStore.builder() + // Build a vector store + // Available storage types are JSON and HASHSET. Default is JSON. + var vectorStore = RedisVectorStore.builder() .withClient(jedis) - .withOptions(new RedisVectorStoreOptions()) + .withOptions( + RedisVectorStoreOptions.builder().withStorageType(RedisStorageType.JSON).build()) .build(); + // Set up the record collection to use String collectionName = "skgithubfiles"; - var collection = vectorStore.getCollection(collectionName, GitHubFile.class, null); + var collection = vectorStore.getCollection(collectionName, + RedisJsonVectorStoreRecordCollectionOptions.builder() + .withRecordClass(GitHubFile.class) + .build()); // Create collection if it does not exist and store data - List ids = collection + collection .createCollectionIfNotExistsAsync() .then(storeData(collection, embeddingGeneration, sampleData())) .block(); - List data = collection.getBatchAsync(ids, null).block(); - - data.forEach(gitHubFile -> System.out.println("Retrieved: " + gitHubFile.getDescription())); + // Search for results + // Might need to wait for the data to be indexed + var results = search("How to get started", collection, embeddingGeneration).block(); - // TODO: Implement search functionality using Redis. + if (results == null || results.getTotalCount() == 0) { + System.out.println("No search results found."); + return; + } + var searchResult = results.getResults().get(0); + System.out.printf("Search result with score: %f.%n Link: %s, Description: %s%n", + searchResult.getScore(), searchResult.getRecord().link, + searchResult.getRecord().description); + } + private static Mono> search( + String searchText, + VectorStoreRecordCollection recordCollection, + OpenAITextEmbeddingGenerationService embeddingGeneration) { + // Generate embeddings for the search text and search for the closest records + return embeddingGeneration.generateEmbeddingAsync(searchText) + .flatMap(r -> recordCollection.searchAsync(r.getVector(), null)); } private static Mono> storeData( @@ -146,6 +176,7 @@ private static Mono> storeData( .flatMap(entry -> { System.out.println("Save '" + entry.getKey() + "' to memory."); + // Generate embeddings for the data and store it return embeddingGeneration .generateEmbeddingsAsync(Collections.singletonList(entry.getValue())) .flatMap(embeddings -> { @@ -176,11 +207,4 @@ private static Map sampleData() { "README: README associated with a sample chat summary react-based webapp" }, }).collect(Collectors.toMap(element -> element[0], element -> element[1])); } - - private static ClientOptions clientOptions() { - return new ClientOptions() - .setTracingOptions(new TracingOptions()) - .setMetricsOptions(new MetricsOptions()) - .setApplicationId("Semantic-Kernel"); - } } diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example07_BingAndGooglePlugins.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example07_BingAndGooglePlugins.java similarity index 97% rename from samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example07_BingAndGooglePlugins.java rename to samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example07_BingAndGooglePlugins.java index 70a8ef284..2e4462fd2 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example07_BingAndGooglePlugins.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example07_BingAndGooglePlugins.java @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.samples.syntaxexamples.plugins; +package com.microsoft.semantickernel.samples.syntaxexamples.plugin; import com.azure.ai.openai.OpenAIAsyncClient; import com.azure.ai.openai.OpenAIClientBuilder; @@ -13,7 +13,7 @@ import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.web.WebSearchEnginePlugin; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.semanticfunctions.KernelPromptTemplateFactory; import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; @@ -84,7 +84,7 @@ private static void example1Async(Kernel kernel, String searchPluginName) { // Run var question = "What's the largest building in the world?"; - var kernelArguments = KernelFunctionArguments.builder() + var kernelArguments = KernelArguments.builder() .withVariable("query", question) .build(); @@ -166,7 +166,7 @@ private static void example2Async(Kernel kernel) { .withDefaultExecutionSettings(promptExecutionSettings) .build(); - var kernelArguments = KernelFunctionArguments.builder() + var kernelArguments = KernelArguments.builder() .withVariable("question", question) .withVariable("externalInformation", "") .build(); @@ -187,7 +187,7 @@ private static void example2Async(Kernel kernel) { System.out.println("Information found:"); System.out.println(information); - kernelArguments = KernelFunctionArguments.builder() + kernelArguments = KernelArguments.builder() .withVariable("question", question) .withVariable("externalInformation", information) .build(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example10_DescribeAllPluginsAndFunctions.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example10_DescribeAllPluginsAndFunctions.java similarity index 97% rename from samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example10_DescribeAllPluginsAndFunctions.java rename to samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example10_DescribeAllPluginsAndFunctions.java index 1205b3354..7e4969d4a 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example10_DescribeAllPluginsAndFunctions.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example10_DescribeAllPluginsAndFunctions.java @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.samples.syntaxexamples.plugins; +package com.microsoft.semantickernel.samples.syntaxexamples.plugin; import java.nio.file.Path; import java.util.Locale; @@ -11,7 +11,6 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.Kernel.Builder; import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; -import com.microsoft.semantickernel.aiservices.openai.textcompletion.OpenAITextGenerationService; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.text.TextPlugin; @@ -21,7 +20,6 @@ import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; -import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; public class Example10_DescribeAllPluginsAndFunctions { diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example13_ConversationSummaryPlugin.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example13_ConversationSummaryPlugin.java similarity index 96% rename from samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example13_ConversationSummaryPlugin.java rename to samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example13_ConversationSummaryPlugin.java index 9cdafbb77..47f0e5a46 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugins/Example13_ConversationSummaryPlugin.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/plugin/Example13_ConversationSummaryPlugin.java @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.samples.syntaxexamples.plugins; +package com.microsoft.semantickernel.samples.syntaxexamples.plugin; import com.azure.ai.openai.OpenAIAsyncClient; import com.azure.ai.openai.OpenAIClientBuilder; @@ -7,15 +7,15 @@ import com.azure.core.credential.KeyCredential; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; -import com.microsoft.semantickernel.aiservices.openai.textcompletion.OpenAITextGenerationService; import com.microsoft.semantickernel.exceptions.ConfigurationException; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.ConversationSummaryPlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments.Builder; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments.Builder; +import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; -import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; import reactor.core.publisher.Mono; /** @@ -107,7 +107,7 @@ public class Example13_ConversationSummaryPlugin { Jane: Darn, it's just repeating stuff now. John: I think we're done. Jane: We're not though! We need like 1500 more characters. - John: Oh Cananda, our home and native land. + John: Oh Canada, our home and native land. Jane: True patriot love in all thy sons command. John: With glowing hearts we see thee rise. Jane: The True North strong and free. @@ -165,7 +165,7 @@ private static void getConversationActionItemsAsync() { conversationSummaryPlugin .get("GetConversationActionItems")) .withArguments( - new Builder() + KernelArguments.builder() .withInput(chatTranscript) .build()); System.out.println("Generated Action Items:"); @@ -181,7 +181,7 @@ private static void getConversationTopicsAsync() { Mono> summary = kernel .invokeAsync(conversationSummaryPlugin.get("GetConversationTopics")) .withArguments( - new Builder() + KernelArguments.builder() .withInput(chatTranscript) .build()); @@ -203,7 +203,7 @@ private static void conversationSummaryPluginAsync() { .getFunctions() .get("SummarizeConversation")) .withArguments( - new Builder() + KernelArguments.builder() .withInput(chatTranscript) .build()) .block(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/rag/DocumentSplittingExample.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/rag/DocumentSplittingExample.java new file mode 100644 index 000000000..549d4f4dd --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/rag/DocumentSplittingExample.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.rag; + +import com.microsoft.semantic.kernel.rag.splitting.Chunk; +import com.microsoft.semantic.kernel.rag.splitting.Document; +import com.microsoft.semantic.kernel.rag.splitting.Splitter; +import com.microsoft.semantic.kernel.rag.splitting.TextSplitter; +import com.microsoft.semantic.kernel.rag.splitting.document.TextDocument; +import com.microsoft.semantic.kernel.rag.splitting.overlap.NoOverlapCondition; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.CountSplitCondition; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.SplitPoint; +import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.apache.pdfbox.io.RandomAccessReadBuffer; +import org.apache.pdfbox.pdfparser.PDFParser; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class DocumentSplittingExample { + + private static String BENEFITS_DOC = "https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/refs/heads/main/data/Benefit_Options.pdf"; + + private static class PDFDocument implements Document { + + private final byte[] pdf; + + private PDFDocument(byte[] pdf) { + this.pdf = pdf; + } + + @Override + public Flux getContent() { + try { + PDFParser parser = new PDFParser( + RandomAccessReadBuffer.createBufferFromStream(new ByteArrayInputStream(pdf))); + PDDocument document = parser.parse(); + String text = new PDFTextStripper().getText(document); + + return Flux.just(text); + } catch (IOException e) { + return Flux.error(e); + } + } + } + + public static void main(String[] args) throws IOException, InterruptedException { + useCustomChunker(); + useInbuiltChunker(); + } + + private static void useInbuiltChunker() throws IOException, InterruptedException { + byte[] pdfBytes = getPdfDoc(); + PDFDocument pdfDoc = new PDFDocument(pdfBytes); + + Splitter splitter = Splitter + .builder() + .maxParagraphsPerChunk(4) + .overlapNPercent(30.0f) + .trimWhitespace() + .build(); + + List chunks = splitter + .splitDocument(pdfDoc) + .collectList() + .block(); + + chunks + .forEach(chunk -> { + System.out.println("========="); + System.out.println(chunk.getContents()); + }); + } + + public static void useCustomChunker() throws IOException, InterruptedException { + + String example = EmbeddedResourceLoader.readFile("example.md", + DocumentSplittingExample.class); + + // Define how we are splitting tokens, in this case we are splitting on headers of an md file + // i.e followed by one or more # characters + TextSplitter textSplitter = (doc, numTokens) -> { + // Split on headers + Pattern pattern = Pattern.compile("(\\r?\\n|\\r)\s*#+", Pattern.MULTILINE); + + Flux splitPoints = Flux.fromStream(pattern.matcher(doc).results()) + .map(window -> window.start()); + + return createWindows(doc, splitPoints); + }; + + // Split into single sections + CountSplitCondition condition = new CountSplitCondition(1, textSplitter); + + Splitter splitter = Splitter + .builder() + .addChunkEndCondition(condition) + // No overlap + .setOverlapCondition(NoOverlapCondition.build()) + // Tidy up the text + .trimWhitespace() + .build(); + + String chunks = splitter + .splitDocument(new TextDocument(example)) + .collectList() + .map(it -> it.stream() + .map(chunk -> chunk.getContents()) + .collect(Collectors.joining("\n============\n"))) + .block(); + + System.out.println(chunks); + } + + /* + * Transforms: [ 2, 10, 20, 100 ] -> [ (0, 2), (2, 10), (10, 20), (20, 100), (100, ) + * ] + */ + private static List createWindows(String doc, Flux splitPoints) { + return Flux.concat( + Flux.just(0), + splitPoints, + Flux.just(doc.length())) + .window(2, 1) + .concatMap(window -> { + return window.collectList() + .flatMap(list -> { + if (list.size() <= 1) { + return Mono.empty(); + } + return Mono.just( + new SplitPoint(list.get(0), list.get(1))); + }); + }) + .collectList() + .block(); + } + + private static byte[] getPdfDoc() throws IOException, InterruptedException { + HttpResponse doc = HttpClient.newHttpClient() + .send(HttpRequest.newBuilder() + .GET() + .uri(URI.create(BENEFITS_DOC)) + .build(), + BodyHandlers.ofByteArray()); + return doc.body(); + } + +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/template/Example56_TemplateMethodFunctionsWithMultipleArguments.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/template/Example56_TemplateMethodFunctionsWithMultipleArguments.java index dc663d2d1..e26babe61 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/template/Example56_TemplateMethodFunctionsWithMultipleArguments.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/template/Example56_TemplateMethodFunctionsWithMultipleArguments.java @@ -10,7 +10,7 @@ import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.text.TextPlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.semanticfunctions.KernelPromptTemplateFactory; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; @@ -53,7 +53,7 @@ public static void main(String[] args) { System.out.println("======== TemplateMethodFunctionsWithMultipleArguments ========"); - var arguments = KernelFunctionArguments.builder() + var arguments = KernelArguments.builder() .withVariable("word2", " Potter") .build(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/template/Example64_MultiplePromptTemplates.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/template/Example64_MultiplePromptTemplates.java index ae534e70f..75568a6e9 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/template/Example64_MultiplePromptTemplates.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/template/Example64_MultiplePromptTemplates.java @@ -9,7 +9,7 @@ import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; import com.microsoft.semantickernel.semanticfunctions.AggregatorPromptTemplateFactory; import com.microsoft.semantickernel.semanticfunctions.HandlebarsPromptTemplateFactory; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.semanticfunctions.KernelPromptTemplateFactory; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateFactory; @@ -75,7 +75,7 @@ public static void runPrompt(Kernel kernel, String templateFormat, String prompt .withPromptTemplateFactory(templateFactory) .build(); - var arguments = KernelFunctionArguments.builder() + var arguments = KernelArguments.builder() .withVariable("name", "Bob") .build(); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/java/applicationinsights.json b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/java/applicationinsights.json new file mode 100644 index 000000000..a5c0a2275 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/java/applicationinsights.json @@ -0,0 +1,16 @@ +{ + "connectionString": "InstrumentationKey=00000000-0000-0000-0000-0000-000000000000", + "preview": { + "processors": [ + { + "type": "span", + "include": { + "matchType": "regexp", + "spanNames": [ + ".*" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/rag/example.md b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/rag/example.md new file mode 100644 index 000000000..725913439 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/rag/example.md @@ -0,0 +1,22 @@ +## Section 1 + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis +aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint +occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Section 2 + +Another section. + +### Subsection 1 + +1, 2, 3, 4, 5, 6, 7, 8, 9, 10. + +# Section 3 + +This is the last section. + +``` +some code +``` \ No newline at end of file diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/log4j2.xml b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/log4j2.xml index 2774f660d..f34bc2011 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/log4j2.xml +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/log4j2.xml @@ -19,6 +19,9 @@ + + + diff --git a/samples/semantickernel-demos/booking-agent-m365/pom.xml b/samples/semantickernel-demos/booking-agent-m365/pom.xml index 0247bef0a..ad08549e4 100644 --- a/samples/semantickernel-demos/booking-agent-m365/pom.xml +++ b/samples/semantickernel-demos/booking-agent-m365/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-demos - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -35,15 +35,6 @@ runtime - - com.microsoft.semantic-kernel - semantickernel-api - - - com.microsoft.semantic-kernel - semantickernel-aiservices-openai - - com.azure azure-identity @@ -52,7 +43,7 @@ com.microsoft.graph microsoft-graph - 6.5.1 + 6.62.0 diff --git a/samples/semantickernel-demos/pom.xml b/samples/semantickernel-demos/pom.xml index 586c79699..ba42e0dbd 100644 --- a/samples/semantickernel-demos/pom.xml +++ b/samples/semantickernel-demos/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-samples-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -12,6 +12,29 @@ pom semantic-kernel-demos + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-aiservices-openai + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + booking-agent-m365 semantickernel-spring-starter diff --git a/samples/semantickernel-demos/semantickernel-spring-starter/pom.xml b/samples/semantickernel-demos/semantickernel-spring-starter/pom.xml index ac17a0db8..29454883a 100644 --- a/samples/semantickernel-demos/semantickernel-spring-starter/pom.xml +++ b/samples/semantickernel-demos/semantickernel-spring-starter/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-demos - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -23,14 +23,6 @@ - - com.microsoft.semantic-kernel - semantickernel-api - - - com.microsoft.semantic-kernel - semantickernel-aiservices-openai - org.slf4j @@ -39,40 +31,40 @@ org.springframework.boot spring-boot-test - 3.2.1 + 4.1.0-M4 test org.assertj assertj-core - 3.25.1 + 4.0.0-M1 test org.springframework.boot spring-boot-autoconfigure - 3.2.1 + 4.1.0-M4 org.springframework.boot spring-boot - 3.2.1 + 4.1.0-M4 org.springframework spring-test - 6.1.2 + 7.0.6 test com.azure azure-identity - 1.12.2 + 1.18.2 org.junit.jupiter junit-jupiter-api - 5.10.1 + 6.1.0-M1 test diff --git a/samples/semantickernel-demos/sk-presidio-sample/pom.xml b/samples/semantickernel-demos/sk-presidio-sample/pom.xml index 40dcdbc07..2684a88ca 100644 --- a/samples/semantickernel-demos/sk-presidio-sample/pom.xml +++ b/samples/semantickernel-demos/sk-presidio-sample/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-demos - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -23,14 +23,6 @@ - - com.microsoft.semantic-kernel - semantickernel-api - - - com.microsoft.semantic-kernel - semantickernel-aiservices-openai - com.microsoft.semantic-kernel semantickernel-presidio-plugin diff --git a/samples/semantickernel-demos/sk-presidio-sample/src/main/java/com/microsoft/semantickernel/Main.java b/samples/semantickernel-demos/sk-presidio-sample/src/main/java/com/microsoft/semantickernel/Main.java index 366d50148..ad14c3caf 100644 --- a/samples/semantickernel-demos/sk-presidio-sample/src/main/java/com/microsoft/semantickernel/Main.java +++ b/samples/semantickernel-demos/sk-presidio-sample/src/main/java/com/microsoft/semantickernel/Main.java @@ -13,7 +13,7 @@ import com.microsoft.semantickernel.presidio.AnonymizedText; import com.microsoft.semantickernel.presidio.AnonymizedTextConverter; import com.microsoft.semantickernel.presidio.RedactorPlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.ServiceNotFoundException; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; @@ -50,7 +50,7 @@ public static void main(String[] args) throws InterruptedException { .invokeAsync("redactor", "redact") .withResultType(AnonymizedText.class) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("input", text) .build()) .block() diff --git a/samples/semantickernel-learn-resources/pom.xml b/samples/semantickernel-learn-resources/pom.xml index e7a1b5168..420b98e30 100644 --- a/samples/semantickernel-learn-resources/pom.xml +++ b/samples/semantickernel-learn-resources/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-samples-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -29,6 +29,22 @@ semantickernel-api + + com.microsoft.semantic-kernel + semantickernel-data-azureaisearch + + + com.microsoft.semantic-kernel + semantickernel-data-jdbc + + + com.microsoft.semantic-kernel + semantickernel-data-redis + + + com.microsoft.semantic-kernel + semantickernel-data-oracle + org.apache.logging.log4j log4j-api @@ -62,6 +78,35 @@ com.microsoft.semantic-kernel semantickernel-aiservices-openai + + com.microsoft.semantic-kernel + semantickernel-experimental + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + com.mysql + mysql-connector-j + 9.6.0 + compile + + + com.microsoft.semantic-kernel + semantickernel-data-postgres + compile + + + com.microsoft.semantic-kernel + semantickernel-api-data + compile + + + com.microsoft.semantic-kernel + semantickernel-api-builders + provided + diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/CreatingFunctions.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/CreatingFunctions.java index 2cbb5baa0..ec3b9b951 100644 --- a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/CreatingFunctions.java +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/CreatingFunctions.java @@ -12,7 +12,7 @@ import com.microsoft.semantickernel.orchestration.ToolCallBehavior; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.MathPlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; import java.util.Scanner; @@ -54,7 +54,7 @@ public static void main(String[] args) { // Test the math plugin var answer = kernel .invokeAsync(kernel.getFunction("MathPlugin", "sqrt")) - .withArguments(KernelFunctionArguments + .withArguments(KernelArguments .builder() .withVariable("number1", 12.0) .build()) diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/FunctionsWithinPrompts.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/FunctionsWithinPrompts.java index f271b7574..de394835d 100644 --- a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/FunctionsWithinPrompts.java +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/FunctionsWithinPrompts.java @@ -16,7 +16,7 @@ import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.ConversationSummaryPlugin; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; @@ -139,7 +139,7 @@ public static void main(String[] args) { System.console().printf("User > "); String request = System.console().readLine(); - KernelFunctionArguments arguments = KernelFunctionArguments.builder() + KernelArguments arguments = KernelArguments.builder() .withVariable("request", request) .withVariable("choices", choices) .withVariable("history", history) @@ -162,7 +162,7 @@ public static void main(String[] args) { // Get chat response FunctionResult chatResult = chat.invokeAsync(kernel) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("request", request) .withVariable("history", history) .build()) diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/SerializingPrompts.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/SerializingPrompts.java index 3b5ba5915..233177e7d 100644 --- a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/SerializingPrompts.java +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/SerializingPrompts.java @@ -11,7 +11,7 @@ import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.ConversationSummaryPlugin; import com.microsoft.semantickernel.semanticfunctions.HandlebarsPromptTemplateFactory; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionYaml; import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; @@ -119,7 +119,7 @@ public static void main(String[] args) throws IOException { // var intent = kernel.invokeAsync(getIntent) - .withArguments(KernelFunctionArguments.builder() + .withArguments(KernelArguments.builder() .withVariable("request", userInput) .withVariable("choices", choices) .withVariable("history", historyString) @@ -134,7 +134,7 @@ public static void main(String[] args) throws IOException { } var reply = kernel.invokeAsync(prompts.get("Chat")) - .withArguments(KernelFunctionArguments.builder() + .withArguments(KernelArguments.builder() .withVariable("request", userInput) .withVariable("history", String.join("\n", diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/Templates.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/Templates.java index 41b096511..b830cd29f 100644 --- a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/Templates.java +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/Templates.java @@ -14,7 +14,7 @@ import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.ConversationSummaryPlugin; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt; import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; @@ -154,7 +154,7 @@ public static void main(String[] args) { System.out.print("User > "); String request = scanner.nextLine(); - KernelFunctionArguments arguments = KernelFunctionArguments.builder() + KernelArguments arguments = KernelArguments.builder() .withVariable("request", request) .withVariable("choices", choices) .withVariable("chatHistory", history) @@ -180,7 +180,7 @@ public static void main(String[] args) { // Get chat response FunctionResult chatResult = chat.invokeAsync(kernel) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("request", request) .withVariable("history", history, chatHistoryType) .build()) diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/UsingTheKernel.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/UsingTheKernel.java index 73f137dc8..ebb0e4979 100644 --- a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/UsingTheKernel.java +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/UsingTheKernel.java @@ -10,7 +10,7 @@ import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; import com.microsoft.semantickernel.samples.plugins.MathPlugin; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; public class UsingTheKernel { @@ -76,7 +76,7 @@ public static void main(String[] args) { var result = poemPlugin.get("ShortPoem") .invokeAsync(kernel) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withInput("The cat sat on a mat") .build()) .withResultType(String.class) @@ -88,7 +88,7 @@ public static void main(String[] args) { var root = mathPlugin.get("sqrt") .invokeAsync(kernel) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withInput(12) .build()) .withResultType(Double.class) diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/index/Hotel.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/index/Hotel.java new file mode 100644 index 000000000..6fff7664f --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/index/Hotel.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.index; + +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; + +import java.util.Collections; +import java.util.List; + +public class Hotel { + @VectorStoreRecordKey + private String hotelId; + + @VectorStoreRecordData(isFilterable = true) + private String name; + + @VectorStoreRecordData(isFullTextSearchable = true) + private String description; + + @VectorStoreRecordVector(dimensions = 4, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.COSINE_DISTANCE) + private List descriptionEmbedding; + + @VectorStoreRecordData(isFilterable = true) + private List tags; + + public Hotel() { + } + + public Hotel(String hotelId, String name, String description, List descriptionEmbedding, + List tags) { + this.hotelId = hotelId; + this.name = name; + this.description = description; + this.descriptionEmbedding = Collections.unmodifiableList(descriptionEmbedding); + this.tags = Collections.unmodifiableList(tags); + } + + public String getHotelId() { + return hotelId; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public List getDescriptionEmbedding() { + return descriptionEmbedding; + } + + public List getTags() { + return tags; + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/index/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/index/Main.java new file mode 100644 index 000000000..414c9be8c --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/index/Main.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.index; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import org.postgresql.ds.PGSimpleDataSource; +import reactor.core.publisher.Mono; + +import java.util.List; + +public class Main { + public static void main(String[] args) { + // Create a PostgreSQL data source + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl("jdbc:postgresql://localhost:5432/sk"); + dataSource.setUser("postgres"); + dataSource.setPassword("root"); + + // Create a JDBC vector store + var vectorStore = JDBCVectorStore.builder() + .withDataSource(dataSource) + .withOptions( + JDBCVectorStoreOptions.builder() + .withQueryProvider(PostgreSQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build()) + .build()) + .build(); + + // Get a collection from the vector store + var collection = vectorStore.getCollection("skhotels", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + + // Create the collection if it doesn't exist yet. + collection.createCollectionAsync().block(); + + // Upsert a record. + var description = "A place where everyone can be happy"; + var hotelId = "1"; + var hotel = new Hotel( + hotelId, + "Hotel Happy", + description, + generateEmbeddingsAsync(description).block(), + List.of("luxury", "pool")); + + collection.upsertAsync(hotel, null).block(); + + // Retrieve the upserted record. + var retrievedHotel = collection.getAsync(hotelId, null).block(); + + // Generate a vector for your search text, using your chosen embedding generation implementation. + // Just showing a placeholder method here for brevity. + var searchVector = generateEmbeddingsAsync( + "I'm looking for a hotel where customer happiness is the priority.").block(); + + // Do the search. + var searchResult = collection.searchAsync(searchVector, VectorSearchOptions.builder() + .withTop(1).build()).block(); + + Hotel record = searchResult.getResults().get(0).getRecord(); + System.out.printf("Found hotel description: %s\n", record.getDescription()); + } + + private static Mono> generateEmbeddingsAsync(String text) { + return Mono.just(List.of(1.0f, 2.0f, 3.0f, 4.0f)); + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/recorddefinition/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/recorddefinition/Main.java new file mode 100644 index 000000000..cf6ad56ba --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/recorddefinition/Main.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.recorddefinition; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorstorage.definition.*; +import com.microsoft.semantickernel.samples.documentationexamples.data.index.Hotel; +import org.postgresql.ds.PGSimpleDataSource; + +import java.util.Arrays; +import java.util.List; + +public class Main { + public static void main(String[] args) { + // Create a PostgreSQL data source + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl("jdbc:postgresql://localhost:5432/sk"); + dataSource.setUser("postgres"); + dataSource.setPassword("root"); + + // Create a JDBC vector store + var vectorStore = JDBCVectorStore.builder() + .withDataSource(dataSource) + .withOptions( + JDBCVectorStoreOptions.builder() + .withQueryProvider(PostgreSQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build()) + .build()) + .build(); + + var hotelDefinition = VectorStoreRecordDefinition.fromFields( + Arrays.asList( + VectorStoreRecordKeyField.builder().withName("hotelId").withFieldType(String.class) + .build(), + VectorStoreRecordDataField.builder() + .withName("name") + .withFieldType(String.class) + .isFilterable(true).build(), + VectorStoreRecordDataField.builder() + .withName("description") + .withFieldType(String.class) + .isFullTextSearchable(true).build(), + VectorStoreRecordVectorField.builder().withName("descriptionEmbedding") + .withDimensions(4) + .withIndexKind(IndexKind.HNSW) + .withDistanceFunction(DistanceFunction.COSINE_DISTANCE) + .withFieldType(List.class).build())); + + var collection = vectorStore.getCollection("skhotels", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordDefinition(hotelDefinition) + .withRecordClass(Hotel.class) + .build()); + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorsearch/hotels/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorsearch/hotels/Main.java new file mode 100644 index 000000000..a53562442 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorsearch/hotels/Main.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.vectorsearch.hotels; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.samples.documentationexamples.data.index.Hotel; +import org.postgresql.ds.PGSimpleDataSource; +import reactor.core.publisher.Mono; + +import java.util.List; + +public class Main { + public static void main(String[] args) { + // Configure the data source + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl("jdbc:postgresql://localhost:5432/sk"); + dataSource.setUser("postgres"); + dataSource.setPassword("root"); + + // Create a JDBC vector store and choose an existing collection that already contains records. + var vectorStore = new JDBCVectorStore(dataSource, JDBCVectorStoreOptions.builder() + .withQueryProvider(PostgreSQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build()) + .build()); + var collection = vectorStore.getCollection("skhotels", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + + // Generate a vector for your search text, using your chosen embedding generation implementation. + // Just showing a placeholder method here for brevity. + var searchVector = generateEmbeddingsAsync( + "I'm looking for a hotel where customer happiness is the priority.").block(); + + // Do the search, passing an options object with a Top value to limit results to the single top match. + var searchResult = collection.searchAsync(searchVector, VectorSearchOptions.builder() + .withTop(1).build()).block(); + + // Inspect the returned hotel. + Hotel hotel = searchResult.getResults().get(0).getRecord(); + System.out.printf("Found hotel description: %s\n", hotel.getDescription()); + } + + private static Mono> generateEmbeddingsAsync(String text) { + return Mono.just(List.of(1.0f, 2.0f, 3.0f, 4.0f)); + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorsearch/products/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorsearch/products/Main.java new file mode 100644 index 000000000..f96593284 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorsearch/products/Main.java @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.vectorsearch.products; + +import com.microsoft.semantickernel.data.VolatileVectorStore; +import com.microsoft.semantickernel.data.VolatileVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Main { + public static void main(String[] args) { + // Build a query provider + var vectorStore = new VolatileVectorStore(); + var collection = vectorStore.getCollection("skproducts", + VolatileVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Product.class) + .build()); + collection.createCollectionIfNotExistsAsync().block(); + var vector = generateEmbeddingsAsync().block(); + collection.upsertAsync( + new Product("1", "Product 1", List.of("Feature 1", "Feature 2"), vector, vector), null) + .block(); + + withVectorFieldName(collection); + withTopAndSkip(collection); + withIncludeVectors(collection); + withVectorSearchFilter(); + } + + public static void withVectorFieldName( + VectorStoreRecordCollection collection) { + // Create the vector search options and indicate that we want to search the FeatureListEmbedding field. + var searchOptions = VectorSearchOptions.builder() + .withVectorFieldName("featureListEmbedding") + .build(); + + // Generate a vector for your search text, using the embedding model of your choice + var searchVector = generateEmbeddingsAsync().block(); + + // Do the search + var searchResult = collection.searchAsync(searchVector, searchOptions).block(); + } + + public static void withTopAndSkip(VectorStoreRecordCollection collection) { + // Create the vector search options and indicate that we want to skip the first 40 results and then get the next 20. + var searchOptions = VectorSearchOptions.builder() + .withTop(20) + .withSkip(40) + .build(); + + // Generate a vector for your search text, using the embedding model of your choice + var searchVector = generateEmbeddingsAsync().block(); + + // Do the search + var searchResult = collection.searchAsync(searchVector, searchOptions).block(); + } + + public static void withIncludeVectors(VectorStoreRecordCollection collection) { + // Create the vector search options and indicate that we want to include vectors in the search results. + var searchOptions = VectorSearchOptions.builder() + .withIncludeVectors(true) + .build(); + + // Generate a vector for your search text, using the embedding model of your choice + var searchVector = generateEmbeddingsAsync().block(); + + // Do the search + var searchResult = collection.searchAsync(searchVector, searchOptions).block(); + } + + public static void withVectorSearchFilter() { + // Build a query provider + var vectorStore = new VolatileVectorStore(); + var collection = vectorStore.getCollection("skglossary", + VolatileVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Glossary.class) + .build()); + collection.createCollectionIfNotExistsAsync().block(); + var vector = generateEmbeddingsAsync().block(); + collection.upsertAsync(new Glossary("1", "External Definitions", List.of("memory"), + "Memory", "The power of the mind to remember things", vector), null).block(); + + // Filter where category == 'External Definitions' and tags contain 'memory'. + var filter = VectorSearchFilter.builder() + .equalTo("category", "External Definitions") + .anyTagEqualTo("tags", "memory") + .build(); + + // Create the vector search options and indicate that we want to filter the search results by a specific field. + var searchOptions = VectorSearchOptions.builder() + .withVectorSearchFilter(filter) + .build(); + + // Generate a vector for your search text, using the embedding model of your choice + var searchVector = generateEmbeddingsAsync().block(); + + // Do the search + var searchResult = collection.searchAsync(searchVector, searchOptions).block(); + } + + public static class Product { + @VectorStoreRecordKey + private String key; + + @VectorStoreRecordData + private String description; + + @VectorStoreRecordData + private List featureList; + + @VectorStoreRecordVector(dimensions = 1536) + public List descriptionEmbedding; + + @VectorStoreRecordVector(dimensions = 1536) + public List featureListEmbedding; + + public Product() { + } + + public Product(String key, String description, List featureList, + List descriptionEmbedding, List featureListEmbedding) { + this.key = key; + this.description = description; + this.featureList = featureList; + this.descriptionEmbedding = Collections.unmodifiableList(descriptionEmbedding); + this.featureListEmbedding = Collections.unmodifiableList(featureListEmbedding); + } + + public String getKey() { + return key; + } + + public String getDescription() { + return description; + } + + public List getFeatureList() { + return featureList; + } + + public List getDescriptionEmbedding() { + return descriptionEmbedding; + } + + public List getFeatureListEmbedding() { + return featureListEmbedding; + } + } + + public static class Glossary { + @VectorStoreRecordKey + private String key; + + @VectorStoreRecordData(isFilterable = true) + private String category; + + @VectorStoreRecordData(isFilterable = true) + private List tags; + + @VectorStoreRecordData + private String term; + + @VectorStoreRecordData + private String definition; + + @VectorStoreRecordVector(dimensions = 1536) + private List definitionEmbedding; + + public Glossary() { + } + + public Glossary(String key, String category, List tags, String term, + String definition, List definitionEmbedding) { + this.key = key; + this.category = category; + this.tags = tags; + this.term = term; + this.definition = definition; + this.definitionEmbedding = Collections.unmodifiableList(definitionEmbedding); + } + + public String getKey() { + return key; + } + + public String getCategory() { + return category; + } + + public List getTags() { + return tags; + } + + public String getTerm() { + return term; + } + + public String getDefinition() { + return definition; + } + + public List getDefinitionEmbedding() { + return definitionEmbedding; + } + } + + private static Mono> generateEmbeddingsAsync() { + return Mono.just(new ArrayList<>(Collections.nCopies(1536, 1.0f))); + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/azureaisearch/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/azureaisearch/Main.java new file mode 100644 index 000000000..89ce88ea3 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/azureaisearch/Main.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.vectorstores.azureaisearch; + +import com.azure.core.credential.AzureKeyCredential; +import com.azure.search.documents.indexes.SearchIndexClientBuilder; +import com.microsoft.semantickernel.data.azureaisearch.AzureAISearchVectorStore; +import com.microsoft.semantickernel.data.azureaisearch.AzureAISearchVectorStoreOptions; +import com.microsoft.semantickernel.data.azureaisearch.AzureAISearchVectorStoreRecordCollection; +import com.microsoft.semantickernel.data.azureaisearch.AzureAISearchVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.samples.documentationexamples.data.index.Hotel; + +public class Main { + public static void main(String[] args) { + // Build the Azure AI Search client + var searchClient = new SearchIndexClientBuilder() + .endpoint("https://.search.windows.net") + .credential(new AzureKeyCredential("")) + .buildAsyncClient(); + + // Build an Azure AI Search Vector Store + var vectorStore = AzureAISearchVectorStore.builder() + .withSearchIndexAsyncClient(searchClient) + .withOptions(new AzureAISearchVectorStoreOptions()) + .build(); + + var collection = new AzureAISearchVectorStoreRecordCollection<>(searchClient, "skhotels", + AzureAISearchVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/inmemory/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/inmemory/Main.java new file mode 100644 index 000000000..fff6e115a --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/inmemory/Main.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.vectorstores.inmemory; + +import com.microsoft.semantickernel.data.VolatileVectorStore; +import com.microsoft.semantickernel.data.VolatileVectorStoreRecordCollection; +import com.microsoft.semantickernel.data.VolatileVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.samples.documentationexamples.data.index.Hotel; + +public class Main { + public static void main(String[] args) { + // Build an Azure AI Search Vector Store + var vectorStore = new VolatileVectorStore(); + + var collection = new VolatileVectorStoreRecordCollection<>("skhotels", + VolatileVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/jdbc/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/jdbc/Main.java new file mode 100644 index 000000000..8b7bc1257 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/jdbc/Main.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.vectorstores.jdbc; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollection; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider; +import com.microsoft.semantickernel.samples.documentationexamples.data.index.Hotel; +import org.postgresql.ds.PGSimpleDataSource; + +public class Main { + public static void main(String[] args) { + // Configure the data source + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl("jdbc:postgresql://localhost:5432/sk"); + dataSource.setUser("postgres"); + dataSource.setPassword("root"); + + // Build a query provider + // Other available query providers are MySQLVectorStoreQueryProvider, SQLiteVectorStoreQueryProvider + // and HSQDBVectorStoreQueryProvider + var queryProvider = PostgreSQLVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + + // Build a vector store + var vectorStore = JDBCVectorStore.builder() + .withDataSource(dataSource) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + var collection = new JDBCVectorStoreRecordCollection<>( + dataSource, + "skhotels", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/oracle/Book.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/oracle/Book.java new file mode 100644 index 000000000..77b034bf4 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/oracle/Book.java @@ -0,0 +1,125 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.samples.documentationexamples.data.vectorstores.oracle; + +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import java.util.List; + +public class Book { + + public Book() { + } + + public Book(String isbn, String title, String author, int pages, + List tags, String summary, List summaryEmbedding) { + this.isbn = isbn; + this.title = title; + this.author = author; + this.pages = pages; + this.tags = tags; + this.summary = summary; + this.summaryEmbedding = summaryEmbedding; + } + + @VectorStoreRecordKey + private String isbn; + + @VectorStoreRecordData(isFilterable = true) + private String title; + + @VectorStoreRecordData(isFilterable = true) + private String author; + + @VectorStoreRecordData + private int pages; + + @VectorStoreRecordData(isFilterable = true) + private List tags; + + @VectorStoreRecordData(isFilterable = true, isFullTextSearchable = true) + private String summary; + + @VectorStoreRecordVector(dimensions = 2) + private List summaryEmbedding; + + public String getIsbn() { + return isbn; + } + + public String getTitle() { + return title; + } + + public String getAuthor() { + return author; + } + + public int getPages() { + return pages; + } + + public List getTags() { + return tags; + } + + public String getSummary() { + return summary; + } + + public List getSummaryEmbedding() { + return summaryEmbedding; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setAuthor(String author) { + this.author = author; + } + + public void setPages(int pages) { + this.pages = pages; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public void setSummaryEmbedding(List summaryEmbedding) { + this.summaryEmbedding = summaryEmbedding; + } + + public void setSummary(String summary) { + this.summary = summary; + } +} \ No newline at end of file diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/oracle/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/oracle/Main.java new file mode 100644 index 000000000..c03f3eb30 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/oracle/Main.java @@ -0,0 +1,101 @@ +/* + ** Oracle Database Vector Store Connector for Semantic Kernel (Java) + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + ** + ** The MIT License (MIT) + ** + ** Permission is hereby granted, free of charge, to any person obtaining a copy + ** of this software and associated documentation files (the "Software"), to + ** deal in the Software without restriction, including without limitation the + ** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + ** sell copies of the Software, and to permit persons to whom the Software is + ** furnished to do so, subject to the following conditions: + ** + ** The above copyright notice and this permission notice shall be included in + ** all copies or substantial portions of the Software. + ** + ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + ** IN THE SOFTWARE. + */ +package com.microsoft.semantickernel.samples.documentationexamples.data.vectorstores.oracle; + +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions; +import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.jdbc.oracle.OracleVectorStoreQueryProvider; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import oracle.jdbc.datasource.impl.OracleDataSource; +import reactor.core.publisher.Mono; + +public class Main { + public static void main(String[] args) throws SQLException { + + // Configure the data source + OracleDataSource dataSource = new OracleDataSource(); + dataSource.setURL("jdbc:oracle:thin:@localhost:1521/FREEPDB1"); + dataSource.setUser("scott"); + dataSource.setPassword("tiger"); + + // Build a query provider + OracleVectorStoreQueryProvider queryProvider = OracleVectorStoreQueryProvider.builder() + .withDataSource(dataSource) + .build(); + + // Build a vector store + JDBCVectorStore vectorStore = JDBCVectorStore.builder() + .withDataSource(dataSource) + .withOptions(JDBCVectorStoreOptions.builder() + .withQueryProvider(queryProvider) + .build()) + .build(); + + VectorStoreRecordCollection collection = vectorStore.getCollection( + "books", + JDBCVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Book.class) + .build()); + + // Create the collection if it doesn't exist yet. + collection.createCollectionIfNotExistsAsync().block(); + + collection.upsertBatchAsync(books, null).block(); + + // Retrieve the upserted record. + Book retrievedBook = collection.getAsync("2", null).block(); + + System.out.println(retrievedBook.getAuthor()); + + // Generate a vector for your search text, using your chosen embedding generation implementation. + // Just showing a placeholder method here for brevity. + List searchVector = generateEmbeddingsAsync( + "I'm looking for a horror book.").block(); + + // Do the search. + VectorSearchResults searchResult = collection.searchAsync( + searchVector, VectorSearchOptions.builder().withTop(1).build()).block(); + + retrievedBook = searchResult.getResults().get(0).getRecord(); + System.out.println("Found Book: " + retrievedBook.getIsbn()); + + } + + static List books = Arrays.asList( + new Book("1", "one", "sking", 0, null, "horror", List.of(1f, 1f)), + new Book("2", "two", "squeen", 0, null, "non-fiction", List.of(-1f, -1f))); + + private static Mono> generateEmbeddingsAsync(String text) { + return Mono.just(List.of(-0.1f, -0.1f)); + } + +} \ No newline at end of file diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/redis/Main.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/redis/Main.java new file mode 100644 index 000000000..418c587c9 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/documentationexamples/data/vectorstores/redis/Main.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.documentationexamples.data.vectorstores.redis; + +import com.microsoft.semantickernel.data.redis.RedisJsonVectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.redis.RedisStorageType; +import com.microsoft.semantickernel.data.redis.RedisVectorStore; +import com.microsoft.semantickernel.data.redis.RedisVectorStoreOptions; +import com.microsoft.semantickernel.samples.documentationexamples.data.index.Hotel; +import redis.clients.jedis.JedisPooled; + +public class Main { + public static void main(String[] args) { + JedisPooled jedis = new JedisPooled(""); + + // Build a Redis Vector Store + // Available storage types are JSON and HASHSET. Default is JSON. + var vectorStore = RedisVectorStore.builder() + .withClient(jedis) + .withOptions( + RedisVectorStoreOptions.builder() + .withStorageType(RedisStorageType.HASH_SET).build()) + .build(); + + var collection = vectorStore.getCollection("skhotels", + RedisJsonVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()); + + collection = vectorStore.getCollection("skhotels", + RedisJsonVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .withPrefixCollectionName(false) + .build()); + + collection.getAsync("myprefix_h1", null).block(); + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/ConversationSummaryPlugin.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/ConversationSummaryPlugin.java index 247fc48c1..0a224c2fd 100644 --- a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/ConversationSummaryPlugin.java +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/ConversationSummaryPlugin.java @@ -5,7 +5,7 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; import com.microsoft.semantickernel.text.TextChunker; @@ -71,7 +71,7 @@ private static Mono processAsync(KernelFunction func, String inp // The first parameter is the input text. return func.invokeAsync(kernel) .withArguments( - new KernelFunctionArguments.Builder() + KernelArguments.builder() .withInput(paragraph) .build()) .withResultType( diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java new file mode 100644 index 000000000..0f8065ee1 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.plugins.github; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public abstract class GitHubModel { + public final static ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + @Override + public String toString() { + try { + return objectMapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public static class User extends GitHubModel { + @JsonProperty("login") + private String login; + @JsonProperty("id") + private long id; + @JsonProperty("name") + private String name; + @JsonProperty("company") + private String company; + @JsonProperty("html_url") + private String url; + + @JsonCreator + public User(@JsonProperty("login") String login, + @JsonProperty("id") long id, + @JsonProperty("name") String name, + @JsonProperty("company") String company, + @JsonProperty("html_url") String url) { + this.login = login; + this.id = id; + this.name = name; + this.company = company; + this.url = url; + } + + public String getLogin() { + return login; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getCompany() { + return company; + } + + public String getUrl() { + return url; + } + } + + public static class Repository extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("full_name") + private String name; + @JsonProperty("description") + private String description; + @JsonProperty("html_url") + private String url; + + @JsonCreator + public Repository(@JsonProperty("id") long id, + @JsonProperty("full_name") String name, + @JsonProperty("description") String description, + @JsonProperty("html_url") String url) { + this.id = id; + this.name = name; + this.description = description; + this.url = url; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getUrl() { + return url; + } + + @Override + public String toString() { + try { + return objectMapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + public static class Issue extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("number") + private long number; + @JsonProperty("title") + private String title; + @JsonProperty("state") + private String state; + @JsonProperty("html_url") + private String url; + @JsonProperty("labels") + private Label[] labels; + @JsonProperty("created_at") + private String createdAt; + @JsonProperty("closed_at") + private String closedAt; + + @JsonCreator + public Issue(@JsonProperty("id") long id, + @JsonProperty("number") long number, + @JsonProperty("title") String title, + @JsonProperty("state") String state, + @JsonProperty("html_url") String url, + @JsonProperty("labels") Label[] labels, + @JsonProperty("created_at") String createdAt, + @JsonProperty("closed_at") String closedAt) { + this.id = id; + this.number = number; + this.title = title; + this.state = state; + this.url = url; + this.labels = labels; + this.createdAt = createdAt; + this.closedAt = closedAt; + } + + public long getId() { + return id; + } + + public long getNumber() { + return number; + } + + public String getTitle() { + return title; + } + + public String getState() { + return state; + } + + public String getUrl() { + return url; + } + + public Label[] getLabels() { + return labels; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getClosedAt() { + return closedAt; + } + } + + public static class IssueDetail extends Issue { + @JsonProperty("body") + private String body; + + @JsonCreator + public IssueDetail(@JsonProperty("id") long id, + @JsonProperty("number") long number, + @JsonProperty("title") String title, + @JsonProperty("state") String state, + @JsonProperty("html_url") String url, + @JsonProperty("labels") Label[] labels, + @JsonProperty("created_at") String createdAt, + @JsonProperty("closed_at") String closedAt, + @JsonProperty("body") String body) { + super(id, number, title, state, url, labels, createdAt, closedAt); + this.body = body; + } + + public String getBody() { + return body; + } + } + + public static class Label extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("name") + private String name; + @JsonProperty("description") + private String description; + + @JsonCreator + public Label(@JsonProperty("id") long id, + @JsonProperty("name") String name, + @JsonProperty("description") String description) { + this.id = id; + this.name = name; + this.description = description; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + } +} diff --git a/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java new file mode 100644 index 000000000..f0bddee10 --- /dev/null +++ b/samples/semantickernel-learn-resources/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.plugins.github; + +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; +import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; + +import java.io.IOException; +import java.util.List; + +public class GitHubPlugin { + public static final String baseUrl = "https://api.github.com"; + private final String token; + + public GitHubPlugin(String token) { + this.token = token; + } + + @DefineKernelFunction(name = "get_user_info", description = "Get user information from GitHub", returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$User") + public Mono getUserProfileAsync() { + HttpClient client = createClient(); + + return makeRequestAsync(client, "/user") + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.User.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubUser", e); + } + }); + } + + @DefineKernelFunction(name = "get_repo_info", description = "Get repository information from GitHub", returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$Repository") + public Mono getRepositoryAsync( + @KernelFunctionParameter(name = "organization", description = "The name of the repository to retrieve information for") String organization, + @KernelFunctionParameter(name = "repo_name", description = "The name of the repository to retrieve information for") String repoName) { + HttpClient client = createClient(); + + return makeRequestAsync(client, String.format("/repos/%s/%s", organization, repoName)) + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.Repository.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubRepository", e); + } + }); + } + + @DefineKernelFunction(name = "get_issues", description = "Get issues from GitHub", returnType = "java.util.List") + public Mono> getIssuesAsync( + @KernelFunctionParameter(name = "organization", description = "The name of the organization to retrieve issues for") String organization, + @KernelFunctionParameter(name = "repo_name", description = "The name of the repository to retrieve issues for") String repoName, + @KernelFunctionParameter(name = "max_results", description = "The maximum number of issues to retrieve", required = false, defaultValue = "10", type = int.class) int maxResults, + @KernelFunctionParameter(name = "state", description = "The state of the issues to retrieve", required = false, defaultValue = "open") String state, + @KernelFunctionParameter(name = "assignee", description = "The assignee of the issues to retrieve", required = false) String assignee) { + HttpClient client = createClient(); + + String query = String.format("/repos/%s/%s/issues", organization, repoName); + query = buildQueryString(query, "state", state); + query = buildQueryString(query, "assignee", assignee); + query = buildQueryString(query, "per_page", String.valueOf(maxResults)); + + return makeRequestAsync(client, query) + .flatMap(json -> { + try { + GitHubModel.Issue[] issues = GitHubModel.objectMapper.readValue(json, + GitHubModel.Issue[].class); + return Mono.just(List.of(issues)); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubIssues", e); + } + }); + } + + @DefineKernelFunction(name = "get_issue_detail_info", description = "Get detail information of a single issue from GitHub", returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$IssueDetail") + public GitHubModel.IssueDetail getIssueDetailAsync( + @KernelFunctionParameter(name = "organization", description = "The name of the repository to retrieve information for") String organization, + @KernelFunctionParameter(name = "repo_name", description = "The name of the repository to retrieve information for") String repoName, + @KernelFunctionParameter(name = "issue_number", description = "The issue number to retrieve information for", type = int.class) int issueNumber) { + HttpClient client = createClient(); + + return makeRequestAsync(client, + String.format("/repos/%s/%s/issues/%d", organization, repoName, issueNumber)) + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.IssueDetail.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubIssue", e); + } + }).block(); + } + + private HttpClient createClient() { + return HttpClient.create() + .baseUrl(baseUrl) + .headers(headers -> { + headers.add("User-Agent", "request"); + headers.add("Accept", "application/vnd.github+json"); + headers.add("Authorization", "Bearer " + token); + headers.add("X-GitHub-Api-Version", "2022-11-28"); + }); + } + + private static String buildQueryString(String path, String param, String value) { + if (value == null || value.isEmpty() + || value.equals(KernelFunctionParameter.NO_DEFAULT_VALUE)) { + return path; + } + + return path + (path.contains("?") ? "&" : "?") + param + "=" + value; + } + + private Mono makeRequestAsync(HttpClient client, String path) { + return client + .get() + .uri(path) + .responseSingle((res, content) -> { + if (res.status().code() != 200) { + return Mono.error(new IllegalStateException("Request failed: " + res.status())); + } + return content.asString(); + }); + } +} diff --git a/samples/semantickernel-sample-plugins/pom.xml b/samples/semantickernel-sample-plugins/pom.xml index 1a5260437..c09c6ea8b 100644 --- a/samples/semantickernel-sample-plugins/pom.xml +++ b/samples/semantickernel-sample-plugins/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-samples-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -12,8 +12,31 @@ pom semantickernel-sample-plugins + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-aiservices-openai + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + semantickernel-openapi-plugin semantickernel-presidio-plugin + semantickernel-text-splitter-plugin diff --git a/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/pom.xml b/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/pom.xml index 5424f2547..4016488ab 100644 --- a/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/pom.xml +++ b/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-sample-plugins - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -62,27 +62,12 @@ io.swagger.parser.v3 swagger-parser - 2.1.22 + 2.1.40 com.microsoft.semantic-kernel semantickernel-aiservices-openai - - org.apache.logging.log4j - log4j-api - test - - - org.apache.logging.log4j - log4j-core - test - - - org.apache.logging.log4j - log4j-slf4j2-impl - test - org.junit.jupiter junit-jupiter-api diff --git a/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/src/main/java/com/microsoft/semantickernel/samples/openapi/OpenAPIHttpRequestPlugin.java b/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/src/main/java/com/microsoft/semantickernel/samples/openapi/OpenAPIHttpRequestPlugin.java index e37b08916..ebdc84495 100644 --- a/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/src/main/java/com/microsoft/semantickernel/samples/openapi/OpenAPIHttpRequestPlugin.java +++ b/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/src/main/java/com/microsoft/semantickernel/samples/openapi/OpenAPIHttpRequestPlugin.java @@ -12,7 +12,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.semantickernel.contextvariables.ContextVariable; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.parameters.Parameter; @@ -66,7 +66,7 @@ public OpenAPIHttpRequestPlugin( * @param arguments The arguments to the http request. * @return The body of the response. */ - public Mono execute(KernelFunctionArguments arguments) { + public Mono execute(KernelArguments arguments) { String body = getBody(arguments); String query = buildQueryString(arguments); String path = buildQueryPath(arguments); @@ -113,7 +113,7 @@ public Mono execute(KernelFunctionArguments arguments) { .doOnNext(response -> LOGGER.debug("Request response: {}", response)); } - private static @Nullable String getBody(KernelFunctionArguments arguments) { + private static @Nullable String getBody(KernelArguments arguments) { String body = null; if (arguments.containsKey("requestbody")) { ContextVariable requestBody = arguments.get("requestbody"); @@ -130,7 +130,7 @@ public Mono execute(KernelFunctionArguments arguments) { return body; } - private String buildQueryPath(KernelFunctionArguments arguments) { + private String buildQueryPath(KernelArguments arguments) { return getParameterStreamOfArguments(arguments) .filter(p -> p instanceof PathParameter) .reduce(path, (path, parameter) -> { @@ -142,7 +142,7 @@ private String buildQueryPath(KernelFunctionArguments arguments) { } private static String getRenderedParameter( - KernelFunctionArguments arguments, String name) { + KernelArguments arguments, String name) { ContextVariable value = arguments.get(name); if (value == null) { @@ -156,7 +156,7 @@ private static String getRenderedParameter( return URLEncoder.encode(rendered, StandardCharsets.US_ASCII); } - private String buildQueryString(KernelFunctionArguments arguments) { + private String buildQueryString(KernelArguments arguments) { return getParameterStreamOfArguments(arguments) .filter(p -> p instanceof QueryParameter) .map(parameter -> { @@ -168,7 +168,7 @@ private String buildQueryString(KernelFunctionArguments arguments) { } private Stream getParameterStreamOfArguments( - KernelFunctionArguments arguments) { + KernelArguments arguments) { if (operation.getParameters() == null) { return Stream.empty(); } diff --git a/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/src/main/java/com/microsoft/semantickernel/samples/openapi/SemanticKernelOpenAPIImporter.java b/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/src/main/java/com/microsoft/semantickernel/samples/openapi/SemanticKernelOpenAPIImporter.java index c1be538dc..2c01f0767 100644 --- a/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/src/main/java/com/microsoft/semantickernel/samples/openapi/SemanticKernelOpenAPIImporter.java +++ b/samples/semantickernel-sample-plugins/semantickernel-openapi-plugin/src/main/java/com/microsoft/semantickernel/samples/openapi/SemanticKernelOpenAPIImporter.java @@ -15,7 +15,7 @@ import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.semanticfunctions.InputVariable; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.OutputVariable; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; @@ -518,7 +518,7 @@ private static KernelFunction buildKernelFunction( try { Method method = OpenAPIHttpRequestPlugin.class.getMethod("execute", - KernelFunctionArguments.class); + KernelArguments.class); return KernelFunction .createFromMethod(method, plugin) diff --git a/samples/semantickernel-sample-plugins/semantickernel-presidio-plugin/pom.xml b/samples/semantickernel-sample-plugins/semantickernel-presidio-plugin/pom.xml index 952ff6afe..af97964db 100644 --- a/samples/semantickernel-sample-plugins/semantickernel-presidio-plugin/pom.xml +++ b/samples/semantickernel-sample-plugins/semantickernel-presidio-plugin/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-sample-plugins - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/pom.xml b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/pom.xml new file mode 100644 index 000000000..5236bcff4 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-sample-plugins + 1.5.1-SNAPSHOT + ../pom.xml + + + semantickernel-text-splitter-plugin + semantickernel-text-splitter-plugin + jar + + + + + com.microsoft.semantic-kernel + semantickernel-bom + ${project.version} + pom + import + + + + + + + com.microsoft.semantic-kernel + semantickernel-api + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + + org.apache.logging.log4j + log4j-api + runtime + + + org.apache.logging.log4j + log4j-core + runtime + + + org.apache.logging.log4j + log4j-slf4j2-impl + runtime + + + com.fasterxml.jackson.core + jackson-databind + compile + + + com.fasterxml.jackson.core + jackson-core + compile + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Chunk.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Chunk.java new file mode 100644 index 000000000..f815656ec --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Chunk.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting; + +public class Chunk { + + private final String chunk; + + public Chunk(String chunk) { + this.chunk = chunk; + } + + public String getContents() { + return chunk; + } + +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/ChunkEndCondition.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/ChunkEndCondition.java new file mode 100644 index 000000000..536b23527 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/ChunkEndCondition.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting; + +/** + * Defines the condition that should be met for a chunk to be considered full. + */ +public interface ChunkEndCondition { + + /** + * Accepts a string and returns the number of character that should be considered as the end of + * the FIRST chunk within the string. This method will be subsequently called until all pages + * are found. + *

+ * Return -1 if the value does not contain enough characters to be considered as a full chunk. + * + * @param value the value to be checked + * @return the index of the character that should be considered as the end of the first chunk in + * the string + */ + public int getEndOfNextChunk(String value); + +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/ChunkPostProcessor.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/ChunkPostProcessor.java new file mode 100644 index 000000000..4a1d19f87 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/ChunkPostProcessor.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting; + +/** + * A post processor that processes a chunk after it has been split. + */ +public interface ChunkPostProcessor { + Chunk process(Chunk chunk); +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Document.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Document.java new file mode 100644 index 000000000..730d27279 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Document.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting; + +import reactor.core.publisher.Flux; + +/** + * A document to be read and split into chunks. + */ +public interface Document { + Flux getContent(); +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/OverlapCondition.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/OverlapCondition.java new file mode 100644 index 000000000..fc8168072 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/OverlapCondition.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting; + +/** + * Defines how much overlap is allowed between two pages. + */ +public interface OverlapCondition { + + /** + * Returns the index of the first character that should be considered as the beginning of the + * overlap. + * + * @param chunk the chunk to be checked + * @return the index of the first character that should be considered as the beginning of the + * overlap + */ + public int getOverlapIndex(String chunk); + +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Splitter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Splitter.java new file mode 100644 index 000000000..5e888c526 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/Splitter.java @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting; + +import com.microsoft.semantic.kernel.rag.splitting.overlap.CountOverlapCondition; +import com.microsoft.semantic.kernel.rag.splitting.overlap.NoOverlapCondition; +import com.microsoft.semantic.kernel.rag.splitting.overlap.PercentageOverlapCondition; +import com.microsoft.semantic.kernel.rag.splitting.postprocessors.RemoveWhitespace; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.CountSplitCondition; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.NewLineSplitter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.ParagraphSplitter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.SentenceSplitter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.WhiteSpaceFilter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.WordSplitter; +import com.microsoft.semantickernel.exceptions.SKException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +/** + * Splits a document into chunks based on supplied chunking strategy. + *

+ * The chunking strategies and conditions are somewhat soft limits. In scenarios where trivial + * chunks or tokens would be formed by the chunking strategy (for instance a chunk of only a few + * words would be formed), these will be merged into the previous chunk. As such it is possible that + * the chunks returned may be larger than the specified chunking strategy would imply, after other + * trivial chunks have been merged into them. + */ +public class Splitter { + + private static final Logger LOGGER = LoggerFactory.getLogger(Splitter.class.getName()); + + private final List chunkEndConditions; + private final OverlapCondition overlapCondition; + private final TrivialChunkFilter trivialChunkFilter; + private final ChunkPostProcessor chunkPostProcessor; + + public Splitter( + List chunkEndConditions, + OverlapCondition overlapCondition, + TrivialChunkFilter trivialChunkFilter, + ChunkPostProcessor chunkPostProcessor) { + this.chunkEndConditions = Collections.unmodifiableList(chunkEndConditions); + this.overlapCondition = overlapCondition; + this.trivialChunkFilter = trivialChunkFilter; + this.chunkPostProcessor = chunkPostProcessor; + } + + /** + * Splits a document into chunks. + * + * @param document the document to split + * @return chunks + */ + public Flux splitDocument(Document document) { + return splitDocument(document, chunkEndConditions, overlapCondition, trivialChunkFilter, + chunkPostProcessor); + } + + /** + * Splits a document into chunks. + * + * @param document the document to split + * @param chunkEndConditions the conditions that determine the end of a chunk + * @param overlapCondition the condition that determines the overlap between chunks + * @param trivialChunkFilter the filter that determines if a chunk is trivial + * @param chunkPostProcessor the post processor to apply to the chunks + * @return a flux of chunks + */ + public static Flux splitDocument( + Document document, + List chunkEndConditions, + OverlapCondition overlapCondition, + TrivialChunkFilter trivialChunkFilter, + ChunkPostProcessor chunkPostProcessor) { + + return document + .getContent() + //TODO: Make the chunking work on true streaming data + .reduce("", (a, b) -> a + b) + .flatMapMany(doc -> { + List chunks = chunkDocument( + chunkEndConditions, + overlapCondition, + trivialChunkFilter, + chunkPostProcessor, + doc); + + return Flux.fromIterable(chunks); + }); + } + + private static List chunkDocument(List chunkEndConditions, + OverlapCondition overlapCondition, TrivialChunkFilter trivialChunkFilter, + ChunkPostProcessor chunkPostProcessor, String doc) { + List chunks = new ArrayList<>(); + + int previousChunkEndIndex = -1; + + while (doc != null && doc.length() > 0) { + + String finalDoc = doc; + + Optional index = chunkEndConditions + .stream() + .map(condition -> condition.getEndOfNextChunk(finalDoc)) + .filter(i -> i != -1) + .min(Integer::compareTo); + + if (index.isPresent()) { + String chunkText = doc.substring(0, index.get()); + + if (chunkText.length() <= previousChunkEndIndex) { + LOGGER.warn( + "This entier chunk consists of overlapped data, this will result in infinite loop. Skipping this chunk."); + + // previous chunk should already contain this text..skip it + doc = doc.substring(Math.min(previousChunkEndIndex, doc.length()), + doc.length()); + + previousChunkEndIndex = -1; + continue; + } + + int overlapIndex = overlapCondition.getOverlapIndex(chunkText); + previousChunkEndIndex = chunkText.length() - overlapIndex; + doc = doc.substring(Math.min(overlapIndex, doc.length()), doc.length()); + + chunks.add(new Chunk(chunkText)); + } else { + chunks.add(new Chunk(doc)); + break; + } + } + + chunks = mergeTrivialChunks(chunks, trivialChunkFilter); + chunks = tidyChunks(chunks, chunkPostProcessor); + return chunks; + } + + /** + * Tidies up the chunks by applying the post processor. + * + * @param chunks the list of chunks to tidy + * @param chunkPostProcessor the post processor to apply + * @return the list of tidied chunks + */ + private static List tidyChunks(List chunks, + ChunkPostProcessor chunkPostProcessor) { + return chunks + .stream() + .map(chunkPostProcessor::process) + .toList(); + } + + /** + * Merges chunks considered trivial into the previous chunk. + * + * @param chunks the list of chunks to merge + * @param trivialChunkFilter the filter to determine if a chunk is trivial + * @return the list of chunks with trivial chunks merged + */ + private static List mergeTrivialChunks(List chunks, + TrivialChunkFilter trivialChunkFilter) { + + List result = new ArrayList<>(); + Chunk current = null; + for (int i = 0; i < chunks.size(); i++) { + if (current == null) { + current = chunks.get(i); + } else { + current = new Chunk( + current.getContents() + chunks.get(i).getContents()); + } + + if (!trivialChunkFilter.isTrivialChunk(current.getContents())) { + result.add(current); + current = null; + } + } + + if (current != null) { + Chunk last = result.remove(result.size() - 1); + result.add(new Chunk(last.getContents() + current.getContents())); + } + + return result; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private List chunkEndConditions = new ArrayList<>(); + private OverlapCondition overlapCondition = new NoOverlapCondition(); + private TrivialChunkFilter trivialChunkFilter = new WhiteSpaceFilter(10); + private ChunkPostProcessor chunkPostProcessor; + + /** + * Splits the document into chunks based on the number of sentences. + * + * @param maxSentencesPerChunk the number of sentences per chunk + * @return the builder + */ + public Builder maxSentencesPerChunk(int maxSentencesPerChunk) { + return addChunkEndCondition( + new CountSplitCondition(maxSentencesPerChunk, new SentenceSplitter())); + } + + /** + * Splits the document into chunks based on the number of words. + * + * @param maxWordsPerChunk the number of words per chunk + * @return the builder + */ + public Builder maxWordsPerChunk(int maxWordsPerChunk) { + return addChunkEndCondition( + new CountSplitCondition(maxWordsPerChunk, new WordSplitter())); + } + + /** + * Splits the document into chunks based on the number of lines. + * + * @param maxLinesPerChunk the number of lines per chunk + * @return the builder + */ + public Builder maxLinesPerChunk(int maxLinesPerChunk) { + return addChunkEndCondition( + new CountSplitCondition(maxLinesPerChunk, new NewLineSplitter())); + } + + /** + * Splits the document into chunks based on the number of paragraphs. + *

+ * NOTE: The ParagraphSplitter is not perfect, see {@link ParagraphSplitter} for more + * information. + * + * @param maxParagraphsPerChunk the number of paragraphs per chunk + * @return the builder + */ + public Builder maxParagraphsPerChunk(int maxParagraphsPerChunk) { + return addChunkEndCondition( + new CountSplitCondition(maxParagraphsPerChunk, new ParagraphSplitter())); + } + + /** + * Overlaps chunks by the given number of lines. + * + * @param overlap the number of characters to overlap + * @return the builder + */ + public Builder overlapNLines(int overlap) { + return setOverlapCondition(new CountOverlapCondition(overlap, new NewLineSplitter())); + } + + /** + * Overlaps chunks by the given number of sentences. + * + * @param overlap the number of sentences to overlap + * @return the builder + */ + public Builder overlapNSentences(int overlap) { + return setOverlapCondition(new CountOverlapCondition(overlap, new SentenceSplitter())); + } + + /** + * Overlaps chunks by the given number of words. + * + * @param overlap the number of words to overlap + * @return the builder + */ + public Builder overlapNWords(int overlap) { + return setOverlapCondition(new CountOverlapCondition(overlap, new WordSplitter())); + } + + /** + * Overlaps chunks by the given percentage. Percentage is calculated based on the number of + * characters in the chunk. Will split at the beginning of the word that gives the required + * percentage. + * + * @param overlap the percentage overlap + * @return the builder + */ + public Builder overlapNPercent(float overlap) { + return setOverlapCondition(new PercentageOverlapCondition(overlap, new WordSplitter())); + } + + /** + * Merges chunks that are less than the given character count. + * + * @param length the length of the chunk + * @return the builder + */ + public Builder mergeChunksLessThanCharCount(int length) { + return setTrivialSplitFilter(new WhiteSpaceFilter(length)); + } + + /** + * Trims whitespace from all chunks. + * + * @return the builder + */ + public Builder trimWhitespace() { + return setChunkPostProcessor(new RemoveWhitespace()); + } + + /** + * Adds a chunk post processor to the builder. This is used to process the chunk after it + * has been split. For example, to remove unwanted whitespace. + * + * @param chunkPostProcessor the post processor to add + * @return the builder + */ + public Builder setChunkPostProcessor(ChunkPostProcessor chunkPostProcessor) { + this.chunkPostProcessor = chunkPostProcessor; + return this; + } + + /** + * Adds a page end condition to the builder. These are applied as OR conditions, i.e the + * page will be the size of the SMALLEST condition. + * + * @param chunkEndCondition + * @return + */ + public Builder addChunkEndCondition(ChunkEndCondition chunkEndCondition) { + chunkEndConditions.add(chunkEndCondition); + return this; + } + + /** + * Adds an overlap condition to the builder. This condition is used to determine the overlap + * between chunks. + * + * @param overlapCondition + * @return the builder + */ + public Builder setOverlapCondition(OverlapCondition overlapCondition) { + this.overlapCondition = overlapCondition; + return this; + } + + /** + * Adds a trivial split filter to the builder. Pages that match this filter will be merged + * into the previous chunk. + * + * @param trivialChunkFilter + * @return the builder + */ + public Builder setTrivialSplitFilter(TrivialChunkFilter trivialChunkFilter) { + this.trivialChunkFilter = trivialChunkFilter; + return this; + } + + public Splitter build() { + if (chunkEndConditions.size() == 0) { + throw new SKException( + "At least one chunk end condition must be provided"); + } + return new Splitter( + chunkEndConditions, + overlapCondition, + trivialChunkFilter, + chunkPostProcessor); + } + + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/TextSplitter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/TextSplitter.java new file mode 100644 index 000000000..e579d3c78 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/TextSplitter.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting; + +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.SplitPoint; +import java.util.List; + +/** + * Interface for splitting text into chunks. + */ +public interface TextSplitter { + + /** + * Get all the split points for the given document. + * + * @param doc the document to split + * @return the split points + */ + default List getSplitPoints(String doc) { + return getNSplitPoints(doc, Integer.MAX_VALUE); + } + + /** + * Get the first n split points for the given document. + * + * @param doc the document to split + * @param n the number of split points to get + * @return the split points + */ + List getNSplitPoints(String doc, int n); +} \ No newline at end of file diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/TrivialChunkFilter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/TrivialChunkFilter.java new file mode 100644 index 000000000..206a6da0d --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/TrivialChunkFilter.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting; + +/** + * A filter that determines if a chunk is trivial and should be merged with the previous chunk. + */ +public interface TrivialChunkFilter { + + /** + * Returns true if the chunk is trivial and should be merged with the previous chunk. + * + * @param doc the chunk to be checked + * @return true if the chunk is trivial + */ + public boolean isTrivialChunk(String doc); + +} \ No newline at end of file diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/document/TextDocument.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/document/TextDocument.java new file mode 100644 index 000000000..046d411dd --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/document/TextDocument.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.document; + +import com.microsoft.semantic.kernel.rag.splitting.Document; +import reactor.core.publisher.Flux; + +/** + * A document that contains a plain text string. + */ +public class TextDocument implements Document { + + private final String document; + + public TextDocument(String document) { + this.document = document; + } + + @Override + public Flux getContent() { + return Flux.just(document); + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/CountOverlapCondition.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/CountOverlapCondition.java new file mode 100644 index 000000000..d3740653e --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/CountOverlapCondition.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.overlap; + +import com.microsoft.semantic.kernel.rag.splitting.OverlapCondition; +import com.microsoft.semantic.kernel.rag.splitting.TextSplitter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.SplitPoint; +import java.util.List; + +/** + * Overlap condition based on counting the number of "splits" i.e if splitting by words, would allow + * you to define an overlap as n words, or if by sentences, then n sentences. + */ +public class CountOverlapCondition implements OverlapCondition { + + // The number of splits to count to consider the condition met + private final int count; + + // The type of splitter to use to get the split points. + private final TextSplitter splitter; + + public CountOverlapCondition(int count, TextSplitter splitter) { + this.count = count; + this.splitter = splitter; + } + + @Override + public int getOverlapIndex(String chunk) { + List splitPoints = splitter.getSplitPoints(chunk); + + if (splitPoints.size() == 0) { + return 0; + } + + int i = Math.max(splitPoints.size() - count, 0); + i = Math.min(splitPoints.size() - 1, i); + + return splitPoints.get(i).getStart(); + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/NoOverlapCondition.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/NoOverlapCondition.java new file mode 100644 index 000000000..696a864cf --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/NoOverlapCondition.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.overlap; + +import com.microsoft.semantic.kernel.rag.splitting.OverlapCondition; + +/** + * An overlap condition that does not overlap. + */ +public class NoOverlapCondition implements OverlapCondition { + + public NoOverlapCondition() { + } + + public static OverlapCondition build() { + return new NoOverlapCondition(); + } + + @Override + public int getOverlapIndex(String chunk) { + return chunk.length(); + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/PercentageOverlapCondition.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/PercentageOverlapCondition.java new file mode 100644 index 000000000..797497081 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/overlap/PercentageOverlapCondition.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.overlap; + +import com.microsoft.semantic.kernel.rag.splitting.OverlapCondition; +import com.microsoft.semantic.kernel.rag.splitting.TextSplitter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.SplitPoint; +import java.util.List; +import org.slf4j.Logger; + +/** + * Overlap condition based on percentage of the characters in the chunk. It will return the full + * split that gives atleast the percentage of the characters in the chunk, i.e if you are splitting + * based on sentence it will return the full sentence. + */ +public class PercentageOverlapCondition implements OverlapCondition { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger( + PercentageOverlapCondition.class); + + private final float percentage; + private final TextSplitter splitter; + + public PercentageOverlapCondition(float percentage, TextSplitter splitter) { + if (percentage < 0 || percentage > 100) { + LOGGER.warn("Percentage must be between 0 and 100, clamping value to this range 100"); + percentage = Math.min(100, Math.max(0, percentage)); + } + this.percentage = percentage; + this.splitter = splitter; + } + + @Override + public int getOverlapIndex(String chunk) { + List splitPoints = splitter.getSplitPoints(chunk); + + float index = chunk.length() * (100.0f - percentage) / 100.0f; + + for (SplitPoint splitPoint : splitPoints) { + if (splitPoint.getEnd() > index) { + return splitPoint.getStart(); + } + } + + return 0; + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/postprocessors/NoOpPostProcessor.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/postprocessors/NoOpPostProcessor.java new file mode 100644 index 000000000..c1697e1a4 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/postprocessors/NoOpPostProcessor.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.postprocessors; + +import com.microsoft.semantic.kernel.rag.splitting.Chunk; +import com.microsoft.semantic.kernel.rag.splitting.ChunkPostProcessor; + +/** + * A post processor that does nothing. + */ +public class NoOpPostProcessor implements ChunkPostProcessor { + + public NoOpPostProcessor() { + } + + @Override + public Chunk process(Chunk chunk) { + return chunk; + } + +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/postprocessors/RemoveWhitespace.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/postprocessors/RemoveWhitespace.java new file mode 100644 index 000000000..497fa664d --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/postprocessors/RemoveWhitespace.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.postprocessors; + +import com.microsoft.semantic.kernel.rag.splitting.Chunk; +import com.microsoft.semantic.kernel.rag.splitting.ChunkPostProcessor; + +/** + * A post processor that removes leading and trailing whitespace from a chunk. + */ +public class RemoveWhitespace implements ChunkPostProcessor { + + @Override + public Chunk process(Chunk chunk) { + return new Chunk(chunk.getContents() + .replaceAll("^\\s+", "") + .replaceAll("^[\n\r]+", "") + .replaceAll("\\s+$", "") + .replaceAll("[\n\r]+$", "")); + } + +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/CountSplitCondition.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/CountSplitCondition.java new file mode 100644 index 000000000..4734cadc0 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/CountSplitCondition.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.splitconditions; + +import com.microsoft.semantic.kernel.rag.splitting.ChunkEndCondition; +import com.microsoft.semantic.kernel.rag.splitting.TextSplitter; +import java.util.List; + +/** + * Overlap condition based on counting the number of "splits" i.e if splitting by words, would allow + * you to define a chunk as n words, or if by sentences, then n sentences. + */ +public class CountSplitCondition implements ChunkEndCondition { + + private final int count; + private final TextSplitter splitter; + + public CountSplitCondition(int count, TextSplitter splitter) { + this.count = count; + this.splitter = splitter; + } + + @Override + public int getEndOfNextChunk(String doc) { + List splitPoints = splitter.getNSplitPoints(doc, count) + .stream() + .filter(it -> it != null) + .filter(it -> it.getEnd() != 0) + .filter(it -> it.getEnd() != it.getStart()) + .filter(it -> it.getStart() != doc.length()) + .toList(); + + if (splitPoints.size() < count) { + return splitPoints.get(splitPoints.size() - 1).getEnd(); + } + + return splitPoints.get(count - 1).getEnd(); + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/NewLineSplitter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/NewLineSplitter.java new file mode 100644 index 000000000..f4f1a3d94 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/NewLineSplitter.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.splitconditions; + +import java.util.regex.Pattern; + +/** + * A splitter that splits text based on new lines. + */ +public class NewLineSplitter extends RegexSplitter { + + private static final Pattern NEW_LINE_SPLIT_REGEX = Pattern + .compile("[(\\r\\n)\\n\\r]+", Pattern.MULTILINE); + + public NewLineSplitter() { + super(NEW_LINE_SPLIT_REGEX); + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/ParagraphSplitter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/ParagraphSplitter.java new file mode 100644 index 000000000..958079f8c --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/ParagraphSplitter.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.splitconditions; + +import java.util.regex.Pattern; + +/** + * A splitter that splits text into paragraphs. + *

+ * This is a very simple and inaccurate splitter that splits text into paragraphs based on either: + *

+ *

    + *
  • Two or more consecutive newlines.
  • + *
  • A line ending in a end sentence character (i.e a period)
  • + *
+ */ +public class ParagraphSplitter extends RegexSplitter { + + private static final Pattern PARAGRAPH_SPLIT_REGEX = Pattern + .compile("[(\\r\\n)\\n\\r]{2,}|([\\.\\.!\\?。]+\\s*[(\\r\\n)\\n\\r])", Pattern.MULTILINE); + + public ParagraphSplitter() { + super(PARAGRAPH_SPLIT_REGEX); + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/RegexSplitter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/RegexSplitter.java new file mode 100644 index 000000000..5828e4a44 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/RegexSplitter.java @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.splitconditions; + +import com.microsoft.semantic.kernel.rag.splitting.TextSplitter; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * A text splitter that uses a regex to find the deliminators + */ +public abstract class RegexSplitter implements TextSplitter { + + public static final int DEFAULT_TRIVIAL_SPLIT_LENGTH = 10; + + private final Pattern pattern; + /** + * Splits below this length are considered trivial and will be merged + */ + private final int trivialSplitLength; + + /** + * Splitter that uses the given regex pattern to split the text + * + * @param pattern the regex pattern to split the text + */ + public RegexSplitter(Pattern pattern) { + this(pattern, DEFAULT_TRIVIAL_SPLIT_LENGTH); + } + + /** + * Splitter that uses the given regex pattern to split the text + * + * @param pattern the regex pattern to split the text + * @param trivialSplitLength the length of a split below which it will be considered trivial and + * will be merged + */ + public RegexSplitter(Pattern pattern, int trivialSplitLength) { + this.pattern = pattern; + this.trivialSplitLength = trivialSplitLength; + } + + @Override + public List getNSplitPoints(String doc, int n) { + Matcher matcher = pattern.matcher(doc); + + List points = matcher.results() + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + + int previousEnd = 0; + for (MatchResult point : points) { + if (isTrivialSplit( + /* start= */ previousEnd, + /* end= */ point.start(), + doc, + trivialSplitLength)) { + continue; + } + result.add(new SplitPoint(previousEnd, point.end())); + previousEnd = point.end(); + if (result.size() >= n) { + break; + } + } + + if (result.size() < n && !isTrivialSplit(previousEnd, doc.length(), doc, 1)) { + result.add(new SplitPoint(previousEnd, doc.length())); + } + + if (result.isEmpty()) { + return List.of(new SplitPoint(0, doc.length())); + } + + return result; + } + + private boolean isTrivialSplit(int start, int end, String doc, int trivialSplitLength) { + String split = doc.substring(start, end); + + // Remove all split characters and whitespace + split = pattern.matcher(split).replaceAll(""); + split = split.replaceAll("\\s+", ""); + + return split.length() <= trivialSplitLength; + + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/SentenceSplitter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/SentenceSplitter.java new file mode 100644 index 000000000..bfe5e7e53 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/SentenceSplitter.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.splitconditions; + +import java.util.regex.Pattern; + +/** + * A splitter that splits text into sentences. + */ +public class SentenceSplitter extends RegexSplitter { + + private static final Pattern SENTENCE_SPLIT_REGEX = Pattern + .compile("[\\.\\.!\\?。]+", Pattern.MULTILINE); + + public SentenceSplitter() { + super(SENTENCE_SPLIT_REGEX); + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/SplitPoint.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/SplitPoint.java new file mode 100644 index 000000000..8f621adb7 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/SplitPoint.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.splitconditions; + +/** + * A class that represents the start and end points of a split. I.e if splitting by word, these + * would be the indices of the first and last char in the word within the chunk. + */ +public class SplitPoint { + + private final int start; + private final int end; + + public SplitPoint(int start, int end) { + this.start = start; + this.end = end; + } + + /** + * The index of the first character in the split. + * + * @return the index of the first character in the split + */ + public int getStart() { + return start; + } + + /** + * The index of the last character in the split. + * + * @return the index of the last character in the split + */ + public int getEnd() { + return end; + } +} \ No newline at end of file diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/WhiteSpaceFilter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/WhiteSpaceFilter.java new file mode 100644 index 000000000..8dabbf40a --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/WhiteSpaceFilter.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.splitconditions; + +import com.microsoft.semantic.kernel.rag.splitting.TrivialChunkFilter; + +/** + * A filter that merges chunks that have less than N non-whitespace characters. + */ +public class WhiteSpaceFilter implements TrivialChunkFilter { + + private final int trivialCharacterCount; + + public WhiteSpaceFilter(int trivialCharacterCount) { + this.trivialCharacterCount = trivialCharacterCount; + } + + @Override + public boolean isTrivialChunk(String doc) { + return doc.replaceAll("\\s+", "").length() < trivialCharacterCount; + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/WordSplitter.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/WordSplitter.java new file mode 100644 index 000000000..8a0e6852f --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/main/java/com/microsoft/semantic/kernel/rag/splitting/splitconditions/WordSplitter.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantic.kernel.rag.splitting.splitconditions; + +import java.util.regex.Pattern; + +/** + * A splitter that splits text into words. + */ +public class WordSplitter extends RegexSplitter { + + private static final Pattern WORD_SPLIT_REGEX = Pattern + .compile("[,、;: ()\\[\\]{}\t\n]+", Pattern.MULTILINE); + + public WordSplitter() { + super(WORD_SPLIT_REGEX, 1); + } +} diff --git a/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/test/java/DocumentSplitTest.java b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/test/java/DocumentSplitTest.java new file mode 100644 index 000000000..edd054919 --- /dev/null +++ b/samples/semantickernel-sample-plugins/semantickernel-text-splitter-plugin/src/test/java/DocumentSplitTest.java @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft. All rights reserved. +import com.microsoft.semantic.kernel.rag.splitting.Chunk; +import com.microsoft.semantic.kernel.rag.splitting.Splitter; +import com.microsoft.semantic.kernel.rag.splitting.document.TextDocument; +import com.microsoft.semantic.kernel.rag.splitting.overlap.CountOverlapCondition; +import com.microsoft.semantic.kernel.rag.splitting.overlap.PercentageOverlapCondition; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.CountSplitCondition; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.NewLineSplitter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.ParagraphSplitter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.SentenceSplitter; +import com.microsoft.semantic.kernel.rag.splitting.splitconditions.WordSplitter; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class DocumentSplitTest { + + private final String NUMBERS = """ + 01 02 03 04 05 06 07 08 09 10. + 11 12 13 14 15 16 17 18 19 20. + 21 22 23 24 25 26 27 28 29 30. + 31 32 33 34 35 36 37 38 39 40. + 41 42 43 44 45 46 47 48 49 50. + 51 52 53 54 55 56 57 58 59 60. + 61 62 63 64 65 66 67 68 69 70. + 71 72 73 74 75 76 77 78 79 80. + 81 82 83 84 85 86 87 88 89 90. + 91 92 93 94 95 96 97 98 99 100. + """.stripIndent(); + + private final String PARAGRAPHS = """ + 01 02 03 04 05 06 07 08 09 10. 11 12 13 14 15 + 16 17 18 19 20. 21 22 23 24 + 25 26 27 28 29 30. + + 31 32 33 34 35 36 37 38 39 40. 41 42 43 44 + 45 46 47 48 49 50. + + 51 52 53 54 55 56 57 58 59 60. 61 + 62 63 64 65 66 67 68 69 70. 71 72 73 + 74 75 76 77 78 79 80. + + + 81 82 83 84 85 86 87 88 89 90. + + + 91 92 93 94 95 96 97 98 99 100. + """.stripIndent(); + + @Test + public void testWordSplit() { + List chunks = Splitter.builder() + .addChunkEndCondition(new CountSplitCondition(9, new WordSplitter())) + .setOverlapCondition(new PercentageOverlapCondition(20.0f, new WordSplitter())) + .trimWhitespace() + .build() + .splitDocument(new TextDocument(NUMBERS)) + .collectList() + .block(); + + Assertions.assertEquals(14, chunks.size()); + + Assertions.assertEquals(""" + 01 02 03 04 05 06 07 08 09""" + .stripIndent(), chunks.get(0).getContents()); + + Assertions.assertEquals(""" + 08 09 10. + 11 12 13 14 15 16""" + .stripIndent(), chunks.get(1).getContents()); + + Assertions.assertEquals(""" + 92 93 94 95 96 97 98 99 100.""" + .stripIndent(), chunks.get(13).getContents()); + } + + @Test + public void testSentenceSplit() { + List chunks = Splitter.builder() + .addChunkEndCondition(new CountSplitCondition(4, new SentenceSplitter())) + .setOverlapCondition(new CountOverlapCondition(2, new SentenceSplitter())) + .trimWhitespace() + .build() + .splitDocument(new TextDocument(NUMBERS)) + .collectList() + .block(); + + Assertions.assertEquals(4, chunks.size()); + + Assertions.assertEquals(""" + 01 02 03 04 05 06 07 08 09 10. + 11 12 13 14 15 16 17 18 19 20. + 21 22 23 24 25 26 27 28 29 30. + 31 32 33 34 35 36 37 38 39 40.""" + .stripIndent(), chunks.get(0).getContents()); + Assertions.assertEquals(""" + 21 22 23 24 25 26 27 28 29 30. + 31 32 33 34 35 36 37 38 39 40. + 41 42 43 44 45 46 47 48 49 50. + 51 52 53 54 55 56 57 58 59 60.""" + .stripIndent(), chunks.get(1).getContents()); + Assertions.assertEquals(""" + 61 62 63 64 65 66 67 68 69 70. + 71 72 73 74 75 76 77 78 79 80. + 81 82 83 84 85 86 87 88 89 90. + 91 92 93 94 95 96 97 98 99 100.""" + .stripIndent(), chunks.get(3).getContents()); + } + + @Test + public void testParagraphSplitter() { + + List chunks = Splitter.builder() + .addChunkEndCondition(new CountSplitCondition(2, new ParagraphSplitter())) + .setOverlapCondition(new CountOverlapCondition(2, new WordSplitter())) + .trimWhitespace() + .build() + .splitDocument(new TextDocument(PARAGRAPHS)) + .collectList() + .block(); + + Assertions.assertEquals( + """ + 01 02 03 04 05 06 07 08 09 10. 11 12 13 14 15 + 16 17 18 19 20. 21 22 23 24 + 25 26 27 28 29 30. + + 31 32 33 34 35 36 37 38 39 40. 41 42 43 44 + 45 46 47 48 49 50.""".stripIndent(), + chunks.get(0).getContents()); + + Assertions.assertEquals( + """ + 89 90. + + + 91 92 93 94 95 96 97 98 99 100.""".stripIndent(), + chunks.get(2).getContents()); + } + + @Test + public void testNewLineSplitter() { + + List chunks = Splitter.builder() + .addChunkEndCondition(new CountSplitCondition(2, new NewLineSplitter())) + .trimWhitespace() + .build() + .splitDocument(new TextDocument(PARAGRAPHS)) + .collectList() + .block(); + + Assertions.assertEquals( + """ + 01 02 03 04 05 06 07 08 09 10. 11 12 13 14 15 + 16 17 18 19 20. 21 22 23 24""".stripIndent(), + chunks.get(0).getContents()); + Assertions.assertEquals( + """ + 25 26 27 28 29 30. + + 31 32 33 34 35 36 37 38 39 40. 41 42 43 44""".stripIndent(), + chunks.get(1).getContents()); + + Assertions.assertEquals( + """ + 81 82 83 84 85 86 87 88 89 90. + + + 91 92 93 94 95 96 97 98 99 100.""".stripIndent(), + chunks.get(4).getContents()); + } +} diff --git a/semantickernel-api-ai-services/pom.xml b/semantickernel-api-ai-services/pom.xml new file mode 100644 index 000000000..e1fd17ef3 --- /dev/null +++ b/semantickernel-api-ai-services/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../pom.xml + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + Semantic Kernel AI Services API + Defines the public interface for the Semantic Kernel Services + + + + com.google.code.findbugs + jsr305 + provided + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + 1 + + + + + + \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/AIService.java b/semantickernel-api-ai-services/src/main/java/com/microsoft/semantickernel/services/AIService.java similarity index 100% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/services/AIService.java rename to semantickernel-api-ai-services/src/main/java/com/microsoft/semantickernel/services/AIService.java diff --git a/semantickernel-api-builders/pom.xml b/semantickernel-api-builders/pom.xml new file mode 100644 index 000000000..cbd35cb81 --- /dev/null +++ b/semantickernel-api-builders/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + + + com.microsoft.semantic-kernel + semantickernel-api-builders + Semantic Kernel Builders API + Defines the public interface for the Semantic Kernel Builders + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + 1 + + + + + + \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SemanticKernelBuilder.java b/semantickernel-api-builders/src/main/java/com/microsoft/semantickernel/builders/SemanticKernelBuilder.java similarity index 100% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SemanticKernelBuilder.java rename to semantickernel-api-builders/src/main/java/com/microsoft/semantickernel/builders/SemanticKernelBuilder.java diff --git a/semantickernel-api-data/pom.xml b/semantickernel-api-data/pom.xml new file mode 100644 index 000000000..9e055c1e8 --- /dev/null +++ b/semantickernel-api-data/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../pom.xml + + + com.microsoft.semantic-kernel + semantickernel-api-data + Semantic Kernel Data API + Defines the public interface for the Semantic Kernel Data + + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + provided + + + com.microsoft.semantic-kernel + semantickernel-api-builders + provided + + + com.microsoft.semantic-kernel + semantickernel-api-textembedding-services + provided + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + provided + + + com.fasterxml.jackson.core + jackson-databind + compile + + + com.fasterxml.jackson.core + jackson-core + compile + + + io.projectreactor + reactor-core + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + 1 + + + + + \ No newline at end of file diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VectorStoreTextSearch.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VectorStoreTextSearch.java new file mode 100644 index 000000000..7ae381115 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VectorStoreTextSearch.java @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data; + +import com.microsoft.semantickernel.data.textsearch.DefaultTextSearchResultMapper; +import com.microsoft.semantickernel.data.textsearch.DefaultTextSearchStringMapper; +import com.microsoft.semantickernel.data.textsearch.KernelSearchResults; +import com.microsoft.semantickernel.data.textsearch.TextSearch; +import com.microsoft.semantickernel.data.textsearch.TextSearchOptions; +import com.microsoft.semantickernel.data.textsearch.TextSearchResult; +import com.microsoft.semantickernel.data.textsearch.TextSearchResultMapper; +import com.microsoft.semantickernel.data.textsearch.TextSearchStringMapper; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorsearch.VectorizedSearch; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.services.textembedding.TextEmbeddingGenerationService; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.stream.Collectors; + +/** + * A text search implementation that uses a vector record collection to perform the search. + * + * @param The record type. + */ +public class VectorStoreTextSearch implements TextSearch { + + private final VectorizedSearch vectorizedSearch; + private final TextEmbeddingGenerationService textEmbeddingGenerationService; + private final TextSearchStringMapper stringMapper; + private final TextSearchResultMapper resultMapper; + private final VectorStoreTextSearchOptions options; + + /** + * Create a new instance of VectorStoreTextSearch with {@link VectorizedSearch} to perform vectorized search and + * {@link TextEmbeddingGenerationService} to generate text embeddings. + * + * @param vectorizedSearch The vectorized search. Usually a vector record collection. + * @param textEmbeddingGenerationService The text embedding generation service. + * @param stringMapper The string mapper. + * @param resultMapper The result mapper. + * @param options The options. + */ + public VectorStoreTextSearch( + @Nonnull VectorizedSearch vectorizedSearch, + @Nonnull TextEmbeddingGenerationService textEmbeddingGenerationService, + @Nullable TextSearchStringMapper stringMapper, + @Nullable TextSearchResultMapper resultMapper, + @Nullable VectorStoreTextSearchOptions options) { + this.vectorizedSearch = vectorizedSearch; + this.textEmbeddingGenerationService = textEmbeddingGenerationService; + this.stringMapper = stringMapper == null ? new DefaultTextSearchStringMapper() + : stringMapper; + this.resultMapper = resultMapper == null ? new DefaultTextSearchResultMapper() + : resultMapper; + this.options = options == null ? new VectorStoreTextSearchOptions() : options; + } + + private Mono> executeSearchAsync(String query, + TextSearchOptions options) { + if (options == null) { + options = TextSearchOptions.createDefault(); + } + + VectorSearchOptions vectorSearchOptions = VectorSearchOptions.builder() + .withVectorSearchFilter(options.getFilter() != null + ? new VectorSearchFilter(options.getFilter().getFilterClauses()) + : null) + .withTop(options.getTop()) + .withSkip(options.getSkip()) + .withIncludeTotalCount(options.isIncludeTotalCount()) + .build(); + + return textEmbeddingGenerationService.generateEmbeddingAsync(query) + .flatMap(embedding -> vectorizedSearch.searchAsync(embedding.getVector(), + vectorSearchOptions)); + } + + /** + * Perform a search for content related to the specified query and return String values representing the search results. + * + * @param query The text to search for. + * @param options The search options. + * @return The search results. + */ + @Override + public Mono> searchAsync(String query, TextSearchOptions options) { + return executeSearchAsync(query, options) + .map(results -> new KernelSearchResults<>( + results.getResults().stream() + .map(r -> stringMapper.fromResultToString(r.getRecord())) + .collect(Collectors.toList()), + results.getTotalCount(), + results.getMetadata())); + } + + /** + * Perform a search for content related to the specified query and return TextSearchResult values representing the search results. + * + * @param query The text to search for. + * @param options The search options. + * @return The search results. + */ + @Override + public Mono> getTextSearchResultsAsync(String query, + TextSearchOptions options) { + return executeSearchAsync(query, options) + .map(results -> new KernelSearchResults<>( + results.getResults().stream() + .map(r -> resultMapper.fromResultToTextSearchResult(r.getRecord())) + .collect(Collectors.toList()), + results.getTotalCount(), + results.getMetadata())); + } + + /** + * Perform a search for content related to the specified query and return Object values representing the search results. + * + * @param query The text to search for. + * @param options The search options. + * @return The search results. + */ + @Override + public Mono> getSearchResultsAsync(String query, + TextSearchOptions options) { + return executeSearchAsync(query, options) + .map(results -> new KernelSearchResults<>( + results.getResults().stream() + .map(r -> resultMapper.fromResultToTextSearchResult(r.getRecord())) + .collect(Collectors.toList()), + results.getTotalCount(), + results.getMetadata())); + } + + /** + * Create a new instance of {@link Builder}. + * + * @param The record type. + * @return The builder. + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * A builder for the {@link VectorStoreTextSearch} class. + * + * @param The record type. + */ + public static class Builder { + @Nullable + private VectorizedSearch vectorizedSearch; + @Nullable + private TextEmbeddingGenerationService textEmbeddingGenerationService; + @Nullable + private TextSearchStringMapper stringMapper; + @Nullable + private TextSearchResultMapper resultMapper; + @Nullable + private VectorStoreTextSearchOptions options; + + /** + * Sets the vectorized search. + * + * @param vectorizedSearch The vectorized search. + * @return The builder. + */ + public Builder withVectorizedSearch(VectorizedSearch vectorizedSearch) { + this.vectorizedSearch = vectorizedSearch; + return this; + } + + /** + * Sets the text embedding generation service. + * + * @param textEmbeddingGenerationService The text embedding generation service. + * @return The builder. + */ + public Builder withTextEmbeddingGenerationService( + TextEmbeddingGenerationService textEmbeddingGenerationService) { + this.textEmbeddingGenerationService = textEmbeddingGenerationService; + return this; + } + + /** + * Sets the string mapper. + * + * @param stringMapper The string mapper. + * @return The builder. + */ + public Builder withStringMapper(TextSearchStringMapper stringMapper) { + this.stringMapper = stringMapper; + return this; + } + + /** + * Sets the result mapper. + * + * @param resultMapper The result mapper. + * @return The builder. + */ + public Builder withResultMapper(TextSearchResultMapper resultMapper) { + this.resultMapper = resultMapper; + return this; + } + + /** + * Sets the options. + * + * @param options The options. + * @return The builder. + */ + public Builder withOptions(VectorStoreTextSearchOptions options) { + this.options = options; + return this; + } + + /** + * Builds the {@link VectorStoreTextSearch} instance. + * + * @return The {@link VectorStoreTextSearch} instance. + */ + public VectorStoreTextSearch build() { + if (vectorizedSearch == null) { + throw new SKException("Vectorized search is required"); + } + if (textEmbeddingGenerationService == null) { + throw new SKException("Text embedding generation service is required"); + } + + return new VectorStoreTextSearch<>(vectorizedSearch, textEmbeddingGenerationService, + stringMapper, resultMapper, options); + } + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VectorStoreTextSearchOptions.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VectorStoreTextSearchOptions.java new file mode 100644 index 000000000..80a468ce5 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VectorStoreTextSearchOptions.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data; + +/** + * Options for vector store text search. + */ +public class VectorStoreTextSearchOptions { +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStore.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStore.java new file mode 100644 index 000000000..dbb56da41 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStore.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; + +import com.microsoft.semantickernel.data.vectorstorage.VectorStore; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.exceptions.SKException; +import reactor.core.publisher.Mono; + +/** + * Represents a volatile vector store. + * A volatile vector store is an in-memory vector store + * that does not persist data. + */ +public class VolatileVectorStore implements VectorStore { + + private final Map> collections; + + /** + * Creates a new instance of the volatile vector store. + */ + public VolatileVectorStore() { + this.collections = new ConcurrentHashMap<>(); + } + + /** + * Gets a collection from the vector store. + * + * @param collectionName The name of the collection. + * @param options The options for the collection. + * @return The collection. + */ + @Override + public VectorStoreRecordCollection getCollection( + @Nonnull String collectionName, + @Nonnull VectorStoreRecordCollectionOptions options) { + if (options.getKeyClass() != String.class) { + throw new SKException("Volatile only supports string keys"); + } + if (options.getRecordClass() == null) { + throw new SKException("Record class is required"); + } + + return (VectorStoreRecordCollection) new VolatileVectorStoreRecordCollection<>( + collectionName, + collections, + (VolatileVectorStoreRecordCollectionOptions) options); + } + + /** + * Gets the names of all collections in the vector store. + * + * @return A list of collection names. + */ + @Override + public Mono> getCollectionNamesAsync() { + return Mono.just(new ArrayList<>(collections.keySet())); + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreCollectionSearchMapping.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreCollectionSearchMapping.java new file mode 100644 index 000000000..3a5eab6a4 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreCollectionSearchMapping.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.filter.EqualToFilterClause; +import com.microsoft.semantickernel.data.filter.FilterClause; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordField; +import com.microsoft.semantickernel.exceptions.SKException; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Provides methods to filter records based on a {@link VectorSearchFilter}. + */ +public class VolatileVectorStoreCollectionSearchMapping { + + /** + * Filters the records based on the given {@link VectorSearchFilter}. + * + * @param records The records to filter. + * @param filter The filter to apply. + * @param recordDefinition The record definition. + * @param objectMapper The object mapper. + * @param The record type. + * @return The filtered records. + */ + public static List filterRecords(List records, + VectorSearchFilter filter, + VectorStoreRecordDefinition recordDefinition, ObjectMapper objectMapper) { + if (filter == null || filter.getFilterClauses().isEmpty()) { + return records; + } + + return records.stream().filter( + record -> { + JsonNode recordNode = objectMapper.valueToTree(record); + + for (FilterClause filterClause : filter.getFilterClauses()) { + if (filterClause instanceof EqualToFilterClause) { + EqualToFilterClause equalToFilterClause = (EqualToFilterClause) filterClause; + VectorStoreRecordField field = recordDefinition + .getField(equalToFilterClause.getFieldName()); + + Object value = objectMapper.convertValue( + recordNode.get(field.getEffectiveStorageName()), field.getFieldType()); + if (!equalToFilterClause.getValue().equals(value)) { + return false; + } + } else { + throw new SKException(String.format("Unsupported filter clause type '%s'.", + filterClause.getClass().getSimpleName())); + } + } + return true; + }).collect(Collectors.toList()); + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollection.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollection.java similarity index 60% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollection.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollection.java index 7a433dbb9..a8ef8a654 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollection.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollection.java @@ -2,24 +2,40 @@ package com.microsoft.semantickernel.data; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.data.recordoptions.DeleteRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorOperations; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResults; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollection; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.DeleteRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; import com.microsoft.semantickernel.exceptions.SKException; -import reactor.core.publisher.Mono; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import java.util.stream.Stream; + +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +/** + * Represents a volatile vector store record collection. + * + * @param The type of record in the collection. + */ +public class VolatileVectorStoreRecordCollection implements + VectorStoreRecordCollection { -public class VolatileVectorStoreRecordCollection - implements VectorStoreRecordCollection { private static final HashSet> supportedKeyTypes = new HashSet<>( Collections.singletonList(String.class)); private Map> collections; @@ -28,12 +44,17 @@ public class VolatileVectorStoreRecordCollection private final VectorStoreRecordDefinition recordDefinition; private final ObjectMapper objectMapper; + /** + * Creates a new instance of the volatile vector store record collection. + * + * @param collectionName The name of the collection. + * @param options The options for the collection. + */ public VolatileVectorStoreRecordCollection(String collectionName, VolatileVectorStoreRecordCollectionOptions options) { this.collectionName = collectionName; this.options = options; this.collections = new ConcurrentHashMap<>(); - this.objectMapper = new ObjectMapper(); if (options.getRecordDefinition() != null) { this.recordDefinition = options.getRecordDefinition(); @@ -42,10 +63,15 @@ public VolatileVectorStoreRecordCollection(String collectionName, .fromRecordClass(this.options.getRecordClass()); } + if (options.getObjectMapper() == null) { + this.objectMapper = new ObjectMapper(); + } else { + this.objectMapper = options.getObjectMapper(); + } + // Validate the key type VectorStoreRecordDefinition.validateSupportedTypes( - Collections - .singletonList(recordDefinition.getKeyDeclaredField(options.getRecordClass())), + Collections.singletonList(recordDefinition.getKeyField()), supportedKeyTypes); } @@ -82,8 +108,9 @@ public Mono collectionExistsAsync() { * @return A Mono representing the completion of the creation operation. */ @Override - public Mono createCollectionAsync() { - return Mono.fromRunnable(() -> collections.put(collectionName, new ConcurrentHashMap<>())); + public Mono> createCollectionAsync() { + return Mono.fromRunnable(() -> collections.put(collectionName, new ConcurrentHashMap<>())) + .then(Mono.just(this)); } /** @@ -92,9 +119,10 @@ public Mono createCollectionAsync() { * @return A Mono representing the completion of the creation operation. */ @Override - public Mono createCollectionIfNotExistsAsync() { + public Mono> createCollectionIfNotExistsAsync() { return Mono - .fromRunnable(() -> collections.putIfAbsent(collectionName, new ConcurrentHashMap<>())); + .fromRunnable(() -> collections.putIfAbsent(collectionName, new ConcurrentHashMap<>())) + .then(Mono.just(this)); } /** @@ -110,7 +138,7 @@ public Mono deleteCollectionAsync() { /** * Gets a record from the store. * - * @param key The key of the record to get. + * @param key The key of the record to get. * @param options The options for getting the record. * @return A Mono emitting the record. */ @@ -122,7 +150,7 @@ public Mono getAsync(String key, GetRecordOptions options) { /** * Gets a batch of records from the store. * - * @param keys The keys of the records to get. + * @param keys The keys of the records to get. * @param options The options for getting the records. * @return A Mono emitting a list of records. */ @@ -146,7 +174,8 @@ public Mono upsertAsync(Record data, UpsertRecordOptions options) { return Mono.fromCallable(() -> { try { ObjectNode objectNode = objectMapper.valueToTree(data); - String key = objectNode.get(recordDefinition.getKeyField().getName()).asText(); + String key = objectNode + .get(recordDefinition.getKeyField().getEffectiveStorageName()).asText(); getCollection().put(key, data); return key; @@ -172,7 +201,8 @@ public Mono> upsertBatchAsync(List data, UpsertRecordOption return data.stream().map(record -> { try { ObjectNode objectNode = objectMapper.valueToTree(record); - String key = objectNode.get(recordDefinition.getKeyField().getName()).asText(); + String key = objectNode + .get(recordDefinition.getKeyField().getEffectiveStorageName()).asText(); collection.put(key, record); return key; @@ -188,7 +218,7 @@ public Mono> upsertBatchAsync(List data, UpsertRecordOption /** * Deletes a record from the store. * - * @param key The key of the record to delete. + * @param key The key of the record to delete. * @param options The options for deleting the record. * @return A Mono representing the completion of the deletion operation. */ @@ -219,4 +249,52 @@ private Map getCollection() { } return (Map) collections.get(collectionName); } + + private List arrayNodeToFloatList(ArrayNode arrayNode) { + return Stream.iterate(0, i -> i + 1) + .limit(arrayNode.size()) + .map(i -> arrayNode.get(i).floatValue()) + .collect(Collectors.toList()); + } + + /** + * Vectorized search. This method searches for records that are similar to the given vector. + * + * @param vector The vector to search with. + * @param options The options to use for the search. + * @return A list of search results. + */ + @Override + public Mono> searchAsync(List vector, + final VectorSearchOptions options) { + if (recordDefinition.getVectorFields().isEmpty()) { + throw new SKException("No vector fields defined. Cannot perform vector search"); + } + + return Mono.fromCallable(() -> { + VectorStoreRecordVectorField firstVectorField = recordDefinition.getVectorFields() + .get(0); + VectorSearchOptions effectiveOptions = options == null + ? VectorSearchOptions.createDefault(firstVectorField.getName()) + : options; + + VectorStoreRecordVectorField vectorField = effectiveOptions.getVectorFieldName() == null + ? firstVectorField + : (VectorStoreRecordVectorField) recordDefinition + .getField(effectiveOptions.getVectorFieldName()); + + DistanceFunction distanceFunction = vectorField + .getDistanceFunction() == DistanceFunction.UNDEFINED + ? DistanceFunction.EUCLIDEAN_DISTANCE + : vectorField.getDistanceFunction(); + + List records = VolatileVectorStoreCollectionSearchMapping.filterRecords( + new ArrayList<>(getCollection().values()), effectiveOptions.getVectorSearchFilter(), + recordDefinition, objectMapper); + + return new VectorSearchResults<>( + VectorOperations.exactSimilaritySearch(records, vector, vectorField, + distanceFunction, effectiveOptions)); + }).subscribeOn(Schedulers.boundedElastic()); + } } diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionOptions.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionOptions.java similarity index 60% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionOptions.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionOptions.java index 85bda03ba..0b5779df0 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionOptions.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionOptions.java @@ -1,26 +1,40 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.data; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.data.vectorstorage.VectorStoreRecordCollectionOptions; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public class VolatileVectorStoreRecordCollectionOptions { +/** + * Represents the options for a volatile vector store record collection. + * + * @param the record type + */ +public class VolatileVectorStoreRecordCollectionOptions + implements VectorStoreRecordCollectionOptions { private final Class recordClass; @Nullable private final VectorStoreRecordDefinition recordDefinition; + @Nullable + private final ObjectMapper objectMapper; /** * Creates a new instance of the Volatile vector store record collection options. * * @param recordClass The record class. * @param recordDefinition The record definition. + * @param objectMapper An instanc of Jackson ObjectMapper. */ + @SuppressFBWarnings("EI_EXPOSE_REP2") // ObjectMapper only has package visibility public VolatileVectorStoreRecordCollectionOptions(@Nonnull Class recordClass, - @Nullable VectorStoreRecordDefinition recordDefinition) { + @Nullable VectorStoreRecordDefinition recordDefinition, ObjectMapper objectMapper) { this.recordClass = recordClass; this.recordDefinition = recordDefinition; + this.objectMapper = objectMapper; } /** @@ -33,6 +47,16 @@ public static Builder builder() { return new Builder<>(); } + /** + * Gets the key class. + * + * @return the key class + */ + @Override + public Class getKeyClass() { + return String.class; + } + /** * Gets the record class. * @@ -42,6 +66,15 @@ public Class getRecordClass() { return recordClass; } + /** + * Gets the object mapper. + * + * @return the object mapper + */ + ObjectMapper getObjectMapper() { + return objectMapper; + } + /** * Gets the record definition. * @@ -61,6 +94,8 @@ public static class Builder { private Class recordClass; @Nullable private VectorStoreRecordDefinition recordDefinition; + @Nullable + private ObjectMapper objectMapper; /** * Sets the record class. @@ -84,6 +119,18 @@ public Builder withRecordDefinition(VectorStoreRecordDefinition recordDe return this; } + /** + * Sets the object mapper. + * + * @param objectMapper the object mapper + * @return the builder + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + /** * Builds the options. * @@ -94,7 +141,8 @@ public VolatileVectorStoreRecordCollectionOptions build() { throw new IllegalArgumentException("recordClass is required"); } - return new VolatileVectorStoreRecordCollectionOptions<>(recordClass, recordDefinition); + return new VolatileVectorStoreRecordCollectionOptions<>(recordClass, recordDefinition, + objectMapper); } } } diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/AnyTagEqualToFilterClause.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/AnyTagEqualToFilterClause.java new file mode 100644 index 000000000..81eea80b9 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/AnyTagEqualToFilterClause.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.filter; + +/** + * A filter clause that filters on any tag equal to a value. + */ +public class AnyTagEqualToFilterClause implements FilterClause { + + private final String fieldName; + private final Object value; + + /** + * Creates a new instance of the AnyTagEqualToFilterClause class. + * @param fieldName The field name to filter on. + * @param value The value to filter on. + */ + public AnyTagEqualToFilterClause(String fieldName, Object value) { + this.fieldName = fieldName; + this.value = value; + } + + /** + * Gets the field name to filter on. + * + * @return The field name to filter on. + */ + public String getFieldName() { + return fieldName; + } + + /** + * Gets the value. + * + * @return The value. + */ + public Object getValue() { + return value; + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/EqualToFilterClause.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/EqualToFilterClause.java new file mode 100644 index 000000000..c75b04dce --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/EqualToFilterClause.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.filter; + +/** + * A filter clause that filters on a field equal to a value. + */ +public class EqualToFilterClause implements FilterClause { + private final String fieldName; + private final Object value; + + /** + * Initializes a new instance of the EqualToFilterClause class. + * + * @param fieldName The field name to filter on. + * @param value The value to filter on. + */ + public EqualToFilterClause(String fieldName, Object value) { + this.fieldName = fieldName; + this.value = value; + } + + /** + * Gets the field name to filter on. + * + * @return The field name to filter on. + */ + public String getFieldName() { + return fieldName; + } + + /** + * Gets the value. + * + * @return The value. + */ + public Object getValue() { + return value; + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/FilterClause.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/FilterClause.java new file mode 100644 index 000000000..8e9a6e603 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/FilterClause.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.filter; + +/** + * A filter clause for a query. + */ +public interface FilterClause { +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/FilterMapping.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/FilterMapping.java new file mode 100644 index 000000000..6483ab400 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/filter/FilterMapping.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.filter; + +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; + +public interface FilterMapping { + /** + * Gets the filter string for the given vector search filter and record definition. + * + * @param filter The filter to get the filter string for. + * @param recordDefinition The record definition to get the filter string for. + * @return The filter string. + */ + String getFilter(VectorSearchFilter filter, VectorStoreRecordDefinition recordDefinition); + + /** + * Gets the filter string for the given equal to filter clause. + * + * @param filterClause The equal to filter clause to get the filter string for. + * @return The filter string. + */ + String getEqualToFilter(EqualToFilterClause filterClause); + + /** + * Gets the filter string for the given any tag equal to filter clause. + * + * @param filterClause The any tag equal to filter clause to get the filter string for. + * @return The filter string. + */ + String getAnyTagEqualToFilter(AnyTagEqualToFilterClause filterClause); +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/DefaultTextSearchResultMapper.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/DefaultTextSearchResultMapper.java new file mode 100644 index 000000000..652d2474c --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/DefaultTextSearchResultMapper.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +/** + * Default implementation of {@link TextSearchResultMapper}. + */ +public class DefaultTextSearchResultMapper implements TextSearchResultMapper { + /** + * Maps a search result to a {@link TextSearchResult}. + * + * @param result The search result. + * @return The {@link TextSearchResult}. + */ + @Override + public TextSearchResult fromResultToTextSearchResult(Object result) { + return TextSearchResult.fromRecord(result); + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/DefaultTextSearchStringMapper.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/DefaultTextSearchStringMapper.java new file mode 100644 index 000000000..5d9c05452 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/DefaultTextSearchStringMapper.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +/** + * Default implementation of {@link TextSearchStringMapper}. + */ +public class DefaultTextSearchStringMapper implements TextSearchStringMapper { + /** + * Maps a search result to a string. + * + * @param result The search result. + * @return The string. + */ + @Override + public String fromResultToString(Object result) { + TextSearchResult textSearchResult = TextSearchResult.fromRecord(result); + return textSearchResult.getValue(); + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/KernelSearchResults.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/KernelSearchResults.java new file mode 100644 index 000000000..233439770 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/KernelSearchResults.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * The search results. + * + * @param The type of the search results. + */ +public class KernelSearchResults { + private final List results; + private final long totalCount; + private final Map metadata; + + /** + * Creates a new instance of the KernelSearchResults class. + * + * @param results The search results. + */ + public KernelSearchResults(List results) { + this(results, results.size(), Collections.emptyMap()); + } + + /** + * Creates a new instance of the KernelSearchResults class. + * + * @param results The search results. + * @param totalCount The total count of search results. + * @param metadata The metadata. + */ + public KernelSearchResults(List results, long totalCount, + Map metadata) { + this.results = Collections.unmodifiableList(results); + this.totalCount = totalCount; + this.metadata = Collections.unmodifiableMap(metadata); + } + + /** + * Gets the total count of search results. + * This value represents the total number of results that are available for the current query and not the number of results being returned. + * + * @return The total count of search results. + */ + public long getTotalCount() { + return totalCount; + } + + /** + * Gets the search results. + * + * @return The search results. + */ + public List getResults() { + return results; + } + + /** + * Gets the metadata associated with the search results. + * + * @return The metadata. + */ + public Map getMetadata() { + return metadata; + } + +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearch.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearch.java new file mode 100644 index 000000000..04ca24ade --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearch.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +import reactor.core.publisher.Mono; + +public interface TextSearch { + + /** + * Perform a search for content related to the specified query and return String values representing the search results. + * + * @param query The text to search for. + * @param options The search options. + * @return The search results. + */ + Mono> searchAsync(String query, TextSearchOptions options); + + /** + * Perform a search for content related to the specified query and return TextSearchResult values representing the search results. + * + * @param query The text to search for. + * @param options The search options. + * @return The search results. + */ + Mono> getTextSearchResultsAsync(String query, + TextSearchOptions options); + + /** + * Perform a search for content related to the specified query and return Object values representing the search results. + * + * @param query The text to search for. + * @param options The search options. + * @return The search results. + */ + Mono> getSearchResultsAsync(String query, + TextSearchOptions options); +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchFilter.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchFilter.java new file mode 100644 index 000000000..8e4484482 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchFilter.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +import com.microsoft.semantickernel.data.filter.EqualToFilterClause; +import com.microsoft.semantickernel.data.filter.FilterClause; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a text search filter. + */ +public class TextSearchFilter { + + private final List filterClauses; + + /** + * Creates a new instance of the TextSearchFilter class. + */ + public TextSearchFilter() { + this(Collections.emptyList()); + } + + /** + * Creates a new instance of the TextSearchFilter class. + * + * @param filterClauses The filter clauses. + */ + public TextSearchFilter(List filterClauses) { + this.filterClauses = Collections.unmodifiableList(filterClauses); + } + + /** + * Gets the filter clauses. + * + * @return The filter clauses. + */ + public List getFilterClauses() { + return filterClauses; + } + + /** + * Creates a new instance of the {@link Builder} class. + * + * @return The builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * The builder for the {@link TextSearchFilter} class. + */ + public static class Builder { + private final List filterClauses = new ArrayList<>(); + + /** + * Adds an EqualToFilterClause to the filter. + * + * @param fieldName The field name to filter on. + * @param value The value. + * @return The builder. + */ + public Builder equalTo(String fieldName, Object value) { + filterClauses.add(new EqualToFilterClause(fieldName, value)); + return this; + } + + /** + * Builds the filter. + * + * @return The filter. + */ + public TextSearchFilter build() { + return new TextSearchFilter(filterClauses); + } + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchOptions.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchOptions.java new file mode 100644 index 000000000..816885815 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchOptions.java @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +/** + * Represents the options for a text search. + */ +public class TextSearchOptions { + + /** + * The default number of search results to return. + */ + public static final int DEFAULT_TOP = 3; + private final boolean includeTotalCount; + private final int top; + private final int skip; + private final TextSearchFilter filter; + + /** + * Creates a new instance of the TextSearchOptions class with default values. + * + * @return A new instance of the TextSearchOptions class with default values. + */ + public static TextSearchOptions createDefault() { + return new TextSearchOptions(false, DEFAULT_TOP, 0, null); + } + + /** + * Creates a new instance of the TextSearchOptions class. + * + * @param includeTotalCount A value indicating whether to include the total count of search results. + * @param top The limit of the number of results to return. + * @param skip The offset of the results to return. + * @param filter The search filter. + */ + TextSearchOptions(boolean includeTotalCount, int top, int skip, TextSearchFilter filter) { + this.includeTotalCount = includeTotalCount; + this.top = top; + this.skip = skip; + this.filter = filter; + } + + /** + * Gets a value indicating whether to include the total count of search results. + * + * @return A value indicating whether to include the total count of search results. + */ + public boolean isIncludeTotalCount() { + return includeTotalCount; + } + + /** + * Gets the limit of the number of results to return. + * + * @return The limit of the number of results to return. + */ + public int getTop() { + return top; + } + + /** + * Gets the offset of the results to return. + * + * @return The offset of the results to return. + */ + public int getSkip() { + return skip; + } + + /** + * Gets the search filter. + * + * @return The search filter. + */ + public TextSearchFilter getFilter() { + return filter; + } + + /** + * Creates a new instance of the {@link Builder} class. + * + * @return The builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * The builder for the {@link TextSearchOptions} class. + */ + public static class Builder { + private boolean includeTotalCount = false; + private int top = DEFAULT_TOP; + private int skip = 0; + private TextSearchFilter filter; + + /** + * Sets a value indicating whether to include the total count of search results. + * + * @param includeTotalCount A value indicating whether to include the total count of search results. + * @return The builder. + */ + public Builder withIncludeTotalCount(boolean includeTotalCount) { + this.includeTotalCount = includeTotalCount; + return this; + } + + /** + * Sets the limit of the number of results to return. + * + * @param top The limit of the number of results to return. + * @return The builder. + */ + public Builder withTop(int top) { + this.top = top; + return this; + } + + /** + * Sets the offset of the results to return. + * + * @param skip The offset of the results to return. + * @return The builder. + */ + public Builder withSkip(int skip) { + this.skip = skip; + return this; + } + + /** + * Sets the search filter. + * + * @param filter The search filter. + * @return The builder. + */ + public Builder withFilter(TextSearchFilter filter) { + this.filter = filter; + return this; + } + + /** + * Builds a new instance of the {@link TextSearchOptions} class. + * + * @return A new instance of the TextSearchOptions class. + */ + public TextSearchOptions build() { + return new TextSearchOptions(includeTotalCount, top, skip, filter); + } + } + +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResult.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResult.java new file mode 100644 index 000000000..2a6221f30 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResult.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +import com.microsoft.semantickernel.exceptions.SKException; + +import java.lang.reflect.Field; + +/** + * Represents a text search result. + */ +public class TextSearchResult { + private final String name; + private final String value; + private final String link; + + /** + * Creates a new instance of the TextSearchResult class. + * + * @param name The name of the search result. + * @param value The value of the search result. + * @param link The link of the search result. + */ + TextSearchResult(String name, String value, String link) { + this.name = name; + this.value = value; + this.link = link; + } + + /** + * Gets the name of the search result. + * + * @return The name of the search result. + */ + public String getName() { + return name; + } + + /** + * Gets the value of the search result. + * + * @return The value of the search result. + */ + public String getValue() { + return value; + } + + /** + * Gets the link of the search result. + * + * @return The link of the search result. + */ + public String getLink() { + return link; + } + + /** + * Creates a new instance of the {@link TextSearchResult} class from a record. + * The record should have fields annotated with {@link TextSearchResultName}, {@link TextSearchResultValue}, and {@link TextSearchResultLink}. + * + * @param record The record. + * @return The TextSearchResult. + */ + public static TextSearchResult fromRecord(Object record) { + String name = null, value = null, link = null; + + try { + for (Field field : record.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(TextSearchResultName.class)) { + if (name != null) { + throw new SKException("Multiple fields with @TextSearchResultName found"); + } + + field.setAccessible(true); + name = (String) field.get(record); + } + if (field.isAnnotationPresent(TextSearchResultValue.class)) { + if (value != null) { + throw new SKException("Multiple fields with @TextSearchResultValue found"); + } + + field.setAccessible(true); + value = (String) field.get(record); + } + if (field.isAnnotationPresent(TextSearchResultLink.class)) { + if (link != null) { + throw new SKException("Multiple fields with @TextSearchResultLink found"); + } + + field.setAccessible(true); + link = (String) field.get(record); + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + if (value == null) { + throw new SKException("No field with @TextSearchResultValue found"); + } + + return new TextSearchResult(name, value, link); + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultLink.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultLink.java new file mode 100644 index 000000000..fab7624e8 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultLink.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a property on a record class as the link to the source data. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface TextSearchResultLink { +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultMapper.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultMapper.java new file mode 100644 index 000000000..2fcbe37e9 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultMapper.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +/** + * Maps a search result to a {@link TextSearchResult}. + */ +public interface TextSearchResultMapper { + /** + * Maps a search result to a {@link TextSearchResult}. + * + * @param result The search result. + * @return The {@link TextSearchResult}. + */ + TextSearchResult fromResultToTextSearchResult(Object result); +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultName.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultName.java new file mode 100644 index 000000000..77a3751ec --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultName.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a property on a record class as the name of the source data. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface TextSearchResultName { +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultValue.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultValue.java new file mode 100644 index 000000000..6b3dd568b --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchResultValue.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a property on a record class as the value of the source data. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface TextSearchResultValue { +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchStringMapper.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchStringMapper.java new file mode 100644 index 000000000..c3afa4305 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/textsearch/TextSearchStringMapper.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.textsearch; + +/** + * Maps a search result to a string. + */ +public interface TextSearchStringMapper { + /** + * Maps a search result to a string. + * + * @param result The search result. + * @return The string. + */ + String fromResultToString(Object result); +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorOperations.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorOperations.java new file mode 100644 index 000000000..ba86cb7b7 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorOperations.java @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorsearch; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordVectorField; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import com.microsoft.semantickernel.exceptions.SKException; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Operations for working with vectors. + */ +public final class VectorOperations { + + /** + * Calculates the cosine similarity of two vectors. The vectors must be equal in length and have + * non-zero norm. + * + * @param x First vector, which is not modified + * @param y Second vector, which is not modified + * @return The cosine similarity of the two vectors + */ + public static float cosineSimilarity(@Nonnull List x, @Nonnull List y) { + Objects.requireNonNull(x); + Objects.requireNonNull(y); + + if (x.size() != y.size()) { + throw new SKException("Vectors lengths must be equal"); + } + + float dotProduct = 0.0F; + float normX = 0.0F; + float normY = 0.0F; + + for (int i = 0; i < x.size(); i++) { + dotProduct += x.get(i) * y.get(i); + normX += x.get(i) * x.get(i); + normY += y.get(i) * y.get(i); + } + + if (normX == 0 || normY == 0) { + throw new SKException("Vectors cannot have zero norm"); + } + + return (dotProduct / (float) (Math.sqrt(normX) * Math.sqrt(normY))); + } + + /** + * Calculates the cosine distance of two vectors. The vectors must be equal in length and have + * non-zero norm. + * + * @param x First vector, which is not modified + * @param y Second vector, which is not modified + * @return The cosine distance of the two vectors + */ + public static double cosineDistance(List x, List y) { + return 1.0 - cosineSimilarity(x, y); + } + + /** + * Calculates the Euclidean distance between two vectors. + * + * @param x First vector, which is not modified + * @param y Second vector, which is not modified + * @return The Euclidean distance between the two vectors + */ + public static float euclideanDistance(@Nonnull List x, @Nonnull List y) { + Objects.requireNonNull(x); + Objects.requireNonNull(y); + + if (x.size() != y.size()) { + throw new SKException("Vectors lengths must be equal"); + } + + float sumOfSquaredDifferences = 0.0f; + + for (int i = 0; i < x.size(); ++i) { + float difference = x.get(i) - y.get(i); + sumOfSquaredDifferences += difference * difference; + } + + return (float) Math.sqrt(sumOfSquaredDifferences); + } + + /** + * Divides the elements of the vector by the divisor. + * + * @param vector Vector to divide, which is not modified + * @param divisor Divisor to apply to each element of the vector + * @return A new vector with the elements divided by the divisor + */ + public static List divide(@Nonnull List vector, float divisor) { + Objects.requireNonNull(vector); + if (Float.isNaN(divisor)) { + throw new SKException("Divisor cannot be NaN"); + } + if (divisor == 0f) { + throw new SKException("Divisor cannot be zero"); + } + + return vector.stream().map(x -> x / divisor).collect(Collectors.toList()); + } + + /** + * Calculates the dot product of two vectors. + * + * @param x First vector, which is not modified + * @param y Second vector, which is not modified + * @return The dot product of the two vectors + */ + public static float dot(@Nonnull List x, @Nonnull List y) { + Objects.requireNonNull(x); + Objects.requireNonNull(y); + + if (x.size() != y.size()) { + throw new SKException("Vectors lengths must be equal"); + } + + float result = 0; + for (int i = 0; i < x.size(); ++i) { + result += x.get(i) * y.get(i); + } + + return result; + } + + /** + * Calculates the Euclidean length of a vector. + * + * @param vector Vector to calculate the length of, which is not modified + * @return The Euclidean length of the vector + */ + public static float euclideanLength(@Nonnull List vector) { + Objects.requireNonNull(vector); + return (float) Math.sqrt(dot(vector, vector)); + } + + /** + * Multiplies the elements of the vector by the multiplier. + * + * @param vector Vector to multiply, which is not modified + * @param multiplier Multiplier to apply to each element of the vector + * @return A new vector with the elements multiplied by the multiplier + */ + public static List multiply(@Nonnull List vector, float multiplier) { + Objects.requireNonNull(vector); + if (Float.isNaN(multiplier)) { + throw new SKException("Multiplier cannot be NaN"); + } + if (Float.isInfinite(multiplier)) { + throw new SKException("Multiplier cannot be infinite"); + } + + return vector.stream().map(x -> x * multiplier).collect(Collectors.toList()); + } + + /** + * Normalizes the vector such that the Euclidean length is 1. + * + * @param vector Vector to normalize, which is not modified + * @return A new, normalized vector + */ + public static List normalize(@Nonnull List vector) { + Objects.requireNonNull(vector); + return divide(vector, euclideanLength(vector)); + } + + /** + * Performs an exact similarity search on a list of records using a vector field. + * + * @param records The records to search. + * @param vector The vector to search for. + * @param vectorField The vector field to use for the search. + * @param distanceFunction The distance function to use for the search. + * @param options The search options. + * @param The type of the records. + * @return The search results. + */ + public static List> exactSimilaritySearch( + List records, + List vector, + VectorStoreRecordVectorField vectorField, + DistanceFunction distanceFunction, + VectorSearchOptions options) { + List> results = new ArrayList<>(); + + for (Record record : records) { + List recordVector; + try { + String json = new ObjectMapper().writeValueAsString(record); + ArrayNode arrayNode = (ArrayNode) new ObjectMapper().readTree(json) + .get(vectorField.getEffectiveStorageName()); + + recordVector = Stream.iterate(0, i -> i + 1) + .limit(arrayNode.size()) + .map(i -> arrayNode.get(i).floatValue()) + .collect(Collectors.toList()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + double score; + switch (distanceFunction) { + case COSINE_SIMILARITY: + score = cosineSimilarity(vector, recordVector); + break; + case COSINE_DISTANCE: + score = cosineDistance(vector, recordVector); + break; + case EUCLIDEAN_DISTANCE: + score = euclideanDistance(vector, recordVector); + break; + case DOT_PRODUCT: + score = dot(vector, recordVector); + break; + default: + throw new SKException("Unsupported distance function"); + } + + results.add(new VectorSearchResult<>(record, score)); + } + + Comparator> comparator = Comparator + .comparingDouble(VectorSearchResult::getScore); + // Higher scores are better + if (distanceFunction == DistanceFunction.COSINE_SIMILARITY + || distanceFunction == DistanceFunction.DOT_PRODUCT) { + comparator = comparator.reversed(); + } + + return results.stream() + .sorted(comparator) + .skip(options.getSkip()) + .limit(options.getTop()) + .collect(Collectors.toList()); + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchFilter.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchFilter.java new file mode 100644 index 000000000..7006135f6 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchFilter.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorsearch; + +import com.microsoft.semantickernel.data.filter.EqualToFilterClause; +import com.microsoft.semantickernel.data.filter.FilterClause; +import com.microsoft.semantickernel.data.filter.AnyTagEqualToFilterClause; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A vector search filter. + */ +public class VectorSearchFilter { + + /** + * Creates a new instance of the VectorSearchFilter class. + * + * @return A new instance of the VectorSearchFilter class. + */ + public static VectorSearchFilter createDefault() { + return new VectorSearchFilter(); + } + + private final List filterClauses; + + /** + * Creates a new instance of the VectorSearchFilter class. + */ + public VectorSearchFilter() { + this(Collections.emptyList()); + } + + /** + * Creates a new instance of the VectorSearchFilter class. + * + * @param filterClauses The filter clauses. + */ + public VectorSearchFilter(List filterClauses) { + this.filterClauses = Collections.unmodifiableList(filterClauses); + } + + /** + * Gets the filter clauses. + * + * @return The filter clauses. + */ + public List getFilterClauses() { + return filterClauses; + } + + /** + * Creates a {@link Builder} for the VectorSearchFilter class. + * + * @return A new instance of the VectorSearchFilter Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for the VectorSearchFilter class. + */ + public static class Builder { + private final List filterClauses = new ArrayList<>(); + + /** + * Adds an EqualToFilterClause to the filter. + * + * @param fieldName The field name to filter on. + * @param value The value. + * @return The builder. + */ + public Builder equalTo(String fieldName, Object value) { + filterClauses.add(new EqualToFilterClause(fieldName, value)); + return this; + } + + /** + * Adds an AnyTagEqualToFilterClause to the filter. + * + * @param fieldName The field name to filter on. + * @param value The value. + * @return The builder. + */ + public Builder anyTagEqualTo(String fieldName, Object value) { + filterClauses.add(new AnyTagEqualToFilterClause(fieldName, value)); + return this; + } + + /** + * Builds the VectorSearchFilter. + * + * @return The VectorSearchFilter. + */ + public VectorSearchFilter build() { + return new VectorSearchFilter(filterClauses); + } + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchResult.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchResult.java new file mode 100644 index 000000000..e1f26beef --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchResult.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorsearch; + +/** + * Represents a vector search result. + * @param The type of the record. + */ +public class VectorSearchResult { + private final Record record; + private final double score; + + /** + * Creates a new instance of VectorSearchResult. + * + * @param record The record. + * @param score The score. + */ + public VectorSearchResult(Record record, double score) { + this.record = record; + this.score = score; + } + + /** + * Gets the record. + * + * @return The record. + */ + public Record getRecord() { + return record; + } + + /** + * Gets the score. + * + * @return The score. + */ + public double getScore() { + return score; + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchResults.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchResults.java new file mode 100644 index 000000000..ad926e8f2 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorSearchResults.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorsearch; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * The search results. + * + * @param The type of the search results. + */ +public class VectorSearchResults { + + private final long totalCount; + private final List> results; + private final Map metadata; + + /** + * Creates a new instance of the VectorSearchResults class. + * + * @param results The search results. + */ + public VectorSearchResults(List> results) { + this(results, results.size(), Collections.emptyMap()); + } + + /** + * Creates a new instance of the VectorSearchResults class. + * + * @param results The search results. + * @param totalCount The total count of search results. + * @param metadata The metadata. + */ + public VectorSearchResults(List> results, long totalCount, + Map metadata) { + this.results = Collections.unmodifiableList(results); + this.totalCount = totalCount; + this.metadata = Collections.unmodifiableMap(metadata); + } + + /** + * Gets the total count of search results. + * This value represents the total number of results that are available for the current query and not the number of results being returned. + * + * @return The total count of search results. + */ + public long getTotalCount() { + return totalCount; + } + + /** + * Gets the search results. + * + * @return The search results. + */ + public List> getResults() { + return results; + } + + /** + * Gets the metadata associated with the search results. + * + * @return The metadata. + */ + public Map getMetadata() { + return metadata; + } + +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorizableTextSearch.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorizableTextSearch.java new file mode 100644 index 000000000..df4d38a81 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorizableTextSearch.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorsearch; + +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import reactor.core.publisher.Mono; + +/** + * A vectorizable text search. + * + * @param The record type. + */ +public interface VectorizableTextSearch { + /** + * Vectorizable text search. This method searches for records that are similar to the given text. + * + * @param searchText The text to search with. + * @param options The options to use for the search. + * @return VectorSearchResults. + */ + Mono> searchAsync(String searchText, + VectorSearchOptions options); +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorizedSearch.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorizedSearch.java new file mode 100644 index 000000000..a8b61b1a1 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorsearch/VectorizedSearch.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorsearch; + +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * A vectorized search. + * + * @param The record type. + */ +public interface VectorizedSearch { + + /** + * Vectorized search. This method searches for records that are similar to the given vector. + * + * @param vector The vector to search with. + * @param options The options to use for the search. + * @return Vector search results. + */ + Mono> searchAsync(List vector, + VectorSearchOptions options); +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStore.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStore.java new file mode 100644 index 000000000..de548be40 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStore.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage; + +import java.util.List; +import javax.annotation.Nonnull; + +import reactor.core.publisher.Mono; + +/** + * Represents a vector store. + */ +public interface VectorStore { + + /** + * Gets a collection from the vector store. + * + * @param collectionName The name of the collection. + * @param options The options for the collection. + * @param The key type. + * @param The record type. + * @return The collection. + */ + VectorStoreRecordCollection getCollection( + @Nonnull String collectionName, + @Nonnull VectorStoreRecordCollectionOptions options); + + /** + * Gets the names of all collections in the vector store. + * + * @return A list of collection names. + */ + Mono> getCollectionNamesAsync(); +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStoreRecordCollection.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordCollection.java similarity index 69% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStoreRecordCollection.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordCollection.java index 1466ac351..1eb6124dd 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStoreRecordCollection.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordCollection.java @@ -1,14 +1,21 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data; +package com.microsoft.semantickernel.data.vectorstorage; -import com.microsoft.semantickernel.data.recordoptions.DeleteRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.UpsertRecordOptions; +import com.microsoft.semantickernel.data.vectorsearch.VectorizedSearch; +import com.microsoft.semantickernel.data.vectorstorage.options.DeleteRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import com.microsoft.semantickernel.data.vectorstorage.options.UpsertRecordOptions; +import java.util.List; import reactor.core.publisher.Mono; -import java.util.List; +/** + * Represents a collection of records in a vector store. + * + * @param The type of the key of the records in the collection. + * @param The type of the records in the collection. + */ +public interface VectorStoreRecordCollection extends VectorizedSearch { -public interface VectorStoreRecordCollection { /** * Gets the name of the collection. * @@ -28,14 +35,14 @@ public interface VectorStoreRecordCollection { * * @return A Mono representing the completion of the creation operation. */ - Mono createCollectionAsync(); + Mono> createCollectionAsync(); /** * Creates the collection in the store if it does not exist. * * @return A Mono representing the completion of the creation operation. */ - Mono createCollectionIfNotExistsAsync(); + Mono> createCollectionIfNotExistsAsync(); /** * Deletes the collection from the store. @@ -47,7 +54,7 @@ public interface VectorStoreRecordCollection { /** * Gets a record from the store. * - * @param key The key of the record to get. + * @param key The key of the record to get. * @param options The options for getting the record. * @return A Mono emitting the record. */ @@ -56,7 +63,7 @@ public interface VectorStoreRecordCollection { /** * Gets a batch of records from the store. * - * @param keys The keys of the records to get. + * @param keys The keys of the records to get. * @param options The options for getting the records. * @return A Mono emitting a list of records. */ @@ -65,7 +72,7 @@ public interface VectorStoreRecordCollection { /** * Inserts or updates a record in the store. * - * @param data The record to upsert. + * @param data The record to upsert. * @param options The options for upserting the record. * @return A Mono emitting the key of the upserted record. */ @@ -74,7 +81,7 @@ public interface VectorStoreRecordCollection { /** * Inserts or updates a batch of records in the store. * - * @param data The records to upsert. + * @param data The records to upsert. * @param options The options for upserting the records. * @return A Mono emitting a list of keys of the upserted records. */ @@ -83,7 +90,7 @@ public interface VectorStoreRecordCollection { /** * Deletes a record from the store. * - * @param key The key of the record to delete. + * @param key The key of the record to delete. * @param options The options for deleting the record. * @return A Mono representing the completion of the deletion operation. */ @@ -92,7 +99,7 @@ public interface VectorStoreRecordCollection { /** * Deletes a batch of records from the store. * - * @param keys The keys of the records to delete. + * @param keys The keys of the records to delete. * @param options The options for deleting the records. * @return A Mono representing the completion of the deletion operation. */ diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordCollectionOptions.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordCollectionOptions.java new file mode 100644 index 000000000..926bd984d --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordCollectionOptions.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage; + +import com.microsoft.semantickernel.data.vectorstorage.definition.VectorStoreRecordDefinition; + +/** + * Represents the options for a collection of vector store records. + * + * @param the type of the key + * @param the type of the record + */ +public interface VectorStoreRecordCollectionOptions { + /** + * Gets the key class. + * + * @return the key class + */ + Class getKeyClass(); + + /** + * Gets the record class. + * + * @return the record class + */ + Class getRecordClass(); + + /** + * Gets the record definition. + * + * @return the record definition + */ + VectorStoreRecordDefinition getRecordDefinition(); +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStoreRecordMapper.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordMapper.java similarity index 71% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStoreRecordMapper.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordMapper.java index 09420cf8d..85b14d61f 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStoreRecordMapper.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/VectorStoreRecordMapper.java @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data; +package com.microsoft.semantickernel.data.vectorstorage; -import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions; +import javax.annotation.Nullable; +import java.util.function.BiFunction; import java.util.function.Function; /** @@ -12,8 +14,9 @@ * @param the storage model type */ public class VectorStoreRecordMapper { + @Nullable private final Function recordToStorageModelMapper; - private final Function storageModelToRecordMapper; + private final BiFunction storageModelToRecordMapper; /** * Constructs a new instance of the VectorStoreRecordMapper. @@ -22,8 +25,8 @@ public class VectorStoreRecordMapper { * @param storageModelToRecordMapper the function to convert a storage model to a record */ protected VectorStoreRecordMapper( - Function recordToStorageModelMapper, - Function storageModelToRecordMapper) { + @Nullable Function recordToStorageModelMapper, + BiFunction storageModelToRecordMapper) { this.recordToStorageModelMapper = recordToStorageModelMapper; this.storageModelToRecordMapper = storageModelToRecordMapper; } @@ -33,6 +36,7 @@ protected VectorStoreRecordMapper( * * @return the function to convert a record to a storage model */ + @Nullable public Function getRecordToStorageModelMapper() { return recordToStorageModelMapper; } @@ -42,7 +46,7 @@ public Function getRecordToStorageModelMapper() { * * @return the function to convert a storage model to a record */ - public Function getStorageModelToRecordMapper() { + public BiFunction getStorageModelToRecordMapper() { return storageModelToRecordMapper; } @@ -60,9 +64,10 @@ public StorageModel mapRecordToStorageModel(Record record) { * Converts a storage model to a record. * * @param storageModel the storage model to convert + * @param options the options * @return the record */ - public Record mapStorageModeltoRecord(StorageModel storageModel) { - return getStorageModelToRecordMapper().apply(storageModel); + public Record mapStorageModelToRecord(StorageModel storageModel, GetRecordOptions options) { + return getStorageModelToRecordMapper().apply(storageModel, options); } } diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordData.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordData.java new file mode 100644 index 000000000..4043af26d --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordData.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Represents a data field in a record. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface VectorStoreRecordData { + /** + * Storage name of the field. + * This value is only used when JSON Serialization using Jackson is not supported in a VectorStore. + * When Jackson is supported, @JsonProperty should be used to specify an alternate field name in the storage database. + * @return The storage name of the field. + */ + String storageName() default ""; + + /** + * Whether the field is filterable. + * @return {@code true} if the field is filterable. + */ + boolean isFilterable() default false; + + /** + * Whether the field is full text searchable. + * @return {@code true} if the field is full text searchable. + */ + boolean isFullTextSearchable() default false; +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordKeyAttribute.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordKey.java similarity index 65% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordKeyAttribute.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordKey.java index ffe2451a8..108b6a0c9 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordKeyAttribute.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordKey.java @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recordattributes; +package com.microsoft.semantickernel.data.vectorstorage.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -7,13 +7,14 @@ import java.lang.annotation.Target; /** - * Represents the key attribute in a record. + * Represents the key field in a record. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface VectorStoreRecordKeyAttribute { +public @interface VectorStoreRecordKey { /** * Storage name of the field. + * @return The storage name of the field. */ String storageName() default ""; } diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordVector.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordVector.java new file mode 100644 index 000000000..dfce13522 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/annotations/VectorStoreRecordVector.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.annotations; + +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind; + +import javax.annotation.Nullable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Represents a vector field in a record. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface VectorStoreRecordVector { + + /** + * Number of dimensions in the vector. + * @return The number of dimensions in the vector. + */ + int dimensions(); + + /** + * Storage name of the field. + * @return The storage name of the field. + */ + String storageName() default ""; + + /** + * Type of index to be used for the vector. + * @return The type of index to be used for the vector. + */ + @Nullable + IndexKind indexKind() default IndexKind.UNDEFINED; + + /** + * Distance function to be used for to compute the distance between vectors. + * @return The distance function to be used for to compute the distance between vectors. + */ + @Nullable + DistanceFunction distanceFunction() default DistanceFunction.UNDEFINED; + +} \ No newline at end of file diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/DistanceFunction.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/DistanceFunction.java new file mode 100644 index 000000000..1512db526 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/DistanceFunction.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.definition; + +/** + * Distance functions for vector storage. + */ +public enum DistanceFunction { + /** + * Cosine (angular) similarity function. + */ + COSINE_SIMILARITY("cosineSimilarity"), + /** + * Cosine distance function. 1 - cosine similarity. + */ + COSINE_DISTANCE("cosineDistance"), + /** + * Dot product between two vectors. + */ + DOT_PRODUCT("dotProduct"), + /** + * Euclidean distance function. Also known as L2 distance. + */ + EUCLIDEAN_DISTANCE("euclidean"), + /** + * No distance function specified. It will default to the database's default distance function. + */ + UNDEFINED(null); + + private final String value; + + DistanceFunction(String value) { + this.value = value; + } + + /** + * Gets the function name. + * @return The function name. + */ + public String getValue() { + return value; + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/IndexKind.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/IndexKind.java new file mode 100644 index 000000000..4372d6511 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/IndexKind.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.definition; + +/** + * Represents the kind of index to use for a vector store. + */ +public enum IndexKind { + /** + * Hierarchical Navigable Small World, which performs an approximate nearest neighbour (ANN) search. + */ + HNSW("Hnsw"), + + /** + * Flat index, which performs an exact nearest neighbour search. + * Also referred to as exhaustive k nearest neighbor in some databases. + * High recall accuracy, but slower and more expensive than HNSW. + * Better with smaller datasets. + */ + FLAT("Flat"), + + /** + * Inverted file index, which performs an approximate nearest neighbour (ANN) search. + */ + IVFFLAT("IVFFlat"), + + /** + * No index specified. It will default to the database's default index. + */ + UNDEFINED(null); + + private final String value; + + IndexKind(String value) { + this.value = value; + } + + /** + * Gets the string value of the index kind. + * + * @return the string value of the index kind + */ + public String getValue() { + return value; + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordDataField.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordDataField.java new file mode 100644 index 000000000..713cae29f --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordDataField.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.definition; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents a data field in a record. + */ +public class VectorStoreRecordDataField extends VectorStoreRecordField { + private final boolean isFilterable; + private final boolean isFullTextSearchable; + + /** + * Create a builder for the VectorStoreRecordDataField class. + * @return a new instance of the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a new instance of the VectorStoreRecordDataField class. + * + * @param name the name of the field + * @param storageName the storage name of the field + * @param fieldType the field type + * @param isFilterable a value indicating whether the field is filterable + * @param isFullTextSearchable a value indicating whether the field is full text searchable + */ + public VectorStoreRecordDataField( + @Nonnull String name, + @Nullable String storageName, + @Nonnull Class fieldType, + boolean isFilterable, + boolean isFullTextSearchable) { + super(name, storageName, fieldType); + this.isFilterable = isFilterable; + this.isFullTextSearchable = isFullTextSearchable; + } + + public VectorStoreRecordDataField( + @Nonnull String name, + @Nullable String storageName, + @Nonnull Class fieldType, + @Nonnull Class fieldSubType, + boolean isFilterable, + boolean isFullTextSearchable) { + super(name, storageName, fieldType, fieldSubType); + this.isFilterable = isFilterable; + this.isFullTextSearchable = isFullTextSearchable; + } + + /** + * Gets a value indicating whether the field is filterable. + * + * @return a value indicating whether the field is filterable + */ + public boolean isFilterable() { + return isFilterable; + } + + /** + * Gets a value indicating whether the field is full text searchable. + * + * @return a value indicating whether the field is full text searchable + */ + public boolean isFullTextSearchable() { + return isFullTextSearchable; + } + + /** + * Builder for the VectorStoreRecordDataField class. + */ + public static class Builder + extends VectorStoreRecordField.Builder { + private boolean isFilterable; + private boolean isFullTextSearchable; + + /** + * Sets a value indicating whether the field is filterable. + * + * @param isFilterable a value indicating whether the field is filterable + * @return the builder + */ + public Builder isFilterable(boolean isFilterable) { + this.isFilterable = isFilterable; + return this; + } + + /** + * Sets a value indicating whether the field is full text searchable. + * + * @param isFullTextSearchable a value indicating whether the field is full text searchable + * @return the builder + */ + public Builder isFullTextSearchable(boolean isFullTextSearchable) { + this.isFullTextSearchable = isFullTextSearchable; + return this; + } + + /** + * Builds a new instance of the VectorStoreRecordDataField class. + * + * @return a new instance of the VectorStoreRecordDataField class + */ + @Override + public VectorStoreRecordDataField build() { + if (name == null) { + throw new IllegalArgumentException("name is required"); + } + if (fieldType == null) { + throw new IllegalArgumentException("fieldType is required"); + } + + return new VectorStoreRecordDataField( + name, + storageName, + fieldType, + fieldSubType, + isFilterable, + isFullTextSearchable); + } + } + +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordDefinition.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordDefinition.java new file mode 100644 index 000000000..abb061112 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordDefinition.java @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.definition; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.exceptions.SKException; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a definition of a vector store record. + */ +public class VectorStoreRecordDefinition { + + private final VectorStoreRecordKeyField keyField; + private final List dataFields; + private final List vectorFields; + + // Cached information + private final List nonVectorFields; + private final List allFields; + private final Map allFieldsMap; + + /** + * Gets the key field in the record definition. + * @return VectorStoreRecordKeyField + */ + public VectorStoreRecordKeyField getKeyField() { + return keyField; + } + + /** + * Gets the data fields in the record definition. + * @return List of VectorStoreRecordDataField + */ + public List getDataFields() { + return dataFields; + } + + /** + * Gets the vector fields in the record definition. + * + * @return List of VectorStoreRecordVectorField + */ + public List getVectorFields() { + return vectorFields; + } + + /** + * Gets all fields in the record definition. + * + * @return List of VectorStoreRecordField + */ + public List getAllFields() { + return allFields; + } + + /** + * Gets the non-vector fields in the record definition. + * + * @return List of VectorStoreRecordField + */ + public List getNonVectorFields() { + return nonVectorFields; + } + + /** + * Checks if the record definition contains a field with the specified name. + * + * @param fieldName The name of the field to check. + * @return boolean + */ + public boolean containsField(String fieldName) { + return allFieldsMap.containsKey(fieldName); + } + + /** + * Gets the field with the specified name. + * + * @param fieldName The name of the field to get. + * @return VectorStoreRecordField + */ + public VectorStoreRecordField getField(String fieldName) { + if (!allFieldsMap.containsKey(fieldName)) { + throw new SKException("Field not found: " + fieldName); + } + return allFieldsMap.get(fieldName); + } + + private VectorStoreRecordDefinition( + VectorStoreRecordKeyField keyField, + List dataFields, + List vectorFields) { + this.keyField = keyField; + this.dataFields = Collections.unmodifiableList(dataFields); + this.vectorFields = Collections.unmodifiableList(vectorFields); + this.nonVectorFields = Collections + .unmodifiableList(Stream.concat(Stream.of(keyField), dataFields.stream()) + .collect(Collectors.toList())); + this.allFields = Collections + .unmodifiableList(Stream.concat(nonVectorFields.stream(), vectorFields.stream()) + .collect(Collectors.toList())); + this.allFieldsMap = Collections.unmodifiableMap(allFields.stream() + .collect(Collectors.toMap(VectorStoreRecordField::getName, p -> p))); + } + + private static VectorStoreRecordDefinition checkFields( + List keyFields, + List dataFields, + List vectorFields) { + if (keyFields.size() != 1) { + throw new IllegalArgumentException("Exactly one key field is required"); + } + + return new VectorStoreRecordDefinition(keyFields.iterator().next(), dataFields, + vectorFields); + } + + /** + * Create a VectorStoreRecordDefinition from a collection of fields. + * + * @param fields The fields to create the definition from. + * @return VectorStoreRecordDefinition + */ + public static VectorStoreRecordDefinition fromFields(List fields) { + List keyFields = fields.stream() + .filter(p -> p instanceof VectorStoreRecordKeyField) + .map(p -> (VectorStoreRecordKeyField) p) + .collect(Collectors.toList()); + + List dataFields = fields.stream() + .filter(p -> p instanceof VectorStoreRecordDataField) + .map(p -> (VectorStoreRecordDataField) p) + .collect(Collectors.toList()); + + List vectorFields = fields.stream() + .filter(p -> p instanceof VectorStoreRecordVectorField) + .map(p -> (VectorStoreRecordVectorField) p) + .collect(Collectors.toList()); + + return checkFields(keyFields, dataFields, vectorFields); + } + + /** + * Create a VectorStoreRecordDefinition from a model. + * + * @param recordClass The model class to create the definition from. + * @return VectorStoreRecordDefinition + */ + public static VectorStoreRecordDefinition fromRecordClass(Class recordClass) { + List keyFields = new ArrayList<>(); + List dataFields = new ArrayList<>(); + List vectorFields = new ArrayList<>(); + + for (Field field : recordClass.getDeclaredFields()) { + String storageName = null; + if (field.isAnnotationPresent(JsonProperty.class)) { + storageName = field.getAnnotation(JsonProperty.class).value(); + } + + if (field.isAnnotationPresent(VectorStoreRecordKey.class)) { + VectorStoreRecordKey keyAttribute = field + .getAnnotation(VectorStoreRecordKey.class); + + if (storageName == null) { + storageName = keyAttribute.storageName().isEmpty() ? field.getName() + : keyAttribute.storageName(); + } + keyFields.add(VectorStoreRecordKeyField.builder() + .withName(field.getName()) + .withStorageName(storageName) + .withFieldType(field.getType()) + .build()); + } + + if (field.isAnnotationPresent(VectorStoreRecordData.class)) { + VectorStoreRecordData dataAttribute = field + .getAnnotation(VectorStoreRecordData.class); + + if (storageName == null) { + storageName = dataAttribute.storageName().isEmpty() ? field.getName() + : dataAttribute.storageName(); + } + dataFields.add(VectorStoreRecordDataField.builder() + .withName(field.getName()) + .withStorageName(storageName) + .withFieldType(field.getType(), + List.class.equals(field.getType()) + ? (Class) ((ParameterizedType) field.getGenericType()) + .getActualTypeArguments()[0] + : null) + .isFilterable(dataAttribute.isFilterable()) + .build()); + } + + if (field.isAnnotationPresent(VectorStoreRecordVector.class)) { + VectorStoreRecordVector vectorAttribute = field + .getAnnotation(VectorStoreRecordVector.class); + + if (storageName == null) { + storageName = vectorAttribute.storageName().isEmpty() ? field.getName() + : vectorAttribute.storageName(); + } + vectorFields.add(VectorStoreRecordVectorField.builder() + .withName(field.getName()) + .withStorageName(storageName) + .withFieldType(field.getType(), + List.class.equals(field.getType()) + ? (Class) ((ParameterizedType) field.getGenericType()) + .getActualTypeArguments()[0] + : null) + .withDimensions(vectorAttribute.dimensions()) + .withIndexKind(vectorAttribute.indexKind()) + .withDistanceFunction(vectorAttribute.distanceFunction()) + .build()); + } + } + + return checkFields(keyFields, dataFields, vectorFields); + } + + /** + * Validate that the record class contains only supported field types. + * @param fields The declared fields in the record class. + * @param supportedTypes The supported field types. + * @throws IllegalArgumentException if unsupported field types are found. + */ + public static void validateSupportedTypes(List fields, + Set> supportedTypes) { + Set> unsupportedTypes = new HashSet<>(); + for (VectorStoreRecordField field : fields) { + if (!supportedTypes.contains(field.getFieldType())) { + unsupportedTypes.add(field.getFieldType()); + } + } + if (!unsupportedTypes.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "Unsupported field types found in record class: %s. Supported types: %s", + unsupportedTypes.stream().map(Class::getName).collect(Collectors.joining(", ")), + supportedTypes.stream().map(Class::getName).collect(Collectors.joining(", ")))); + } + } +} diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordField.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordField.java new file mode 100644 index 000000000..f777bd5c1 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordField.java @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.definition; + +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents a field in a record. + */ +public class VectorStoreRecordField { + private final String name; + @Nullable + private final String storageName; + private final Class fieldType; + private final Class fieldSubType; + + /** + * Creates a new instance of the VectorStoreRecordField class. + * + * @param name the name of the field + * @param storageName the storage name of the field + * @param fieldType the field type + */ + public VectorStoreRecordField( + @Nonnull String name, + @Nullable String storageName, + @Nonnull Class fieldType) { + this.name = name; + this.storageName = storageName; + this.fieldType = fieldType; + this.fieldSubType = null; + } + + /** + * Creates a new instance of the VectorStoreRecordField class. + * + * @param name the name of the field + * @param storageName the storage name of the field + * @param fieldType the field type + * @param fieldSubType if the field type is a list, the type of + * the list elements, otherwise null + */ + public VectorStoreRecordField( + @Nonnull String name, + @Nullable String storageName, + @Nonnull Class fieldType, + @Nonnull Class fieldSubType) { + this.name = name; + this.storageName = storageName; + this.fieldType = fieldType; + this.fieldSubType = fieldSubType; + } + + /** + * Gets the name of the field. + * + * @return the name of the field + */ + public String getName() { + return name; + } + + /** + * Gets the storage name of the field. + * + * @return the storage name of the field + */ + public String getStorageName() { + return storageName; + } + + /** + * Gets the effective storage name of the field. + *

+ * If the storage name is not set, the name of the field is returned. + * @return the effective storage name of the field + */ + public String getEffectiveStorageName() { + return storageName != null ? storageName : name; + } + + /** + * Gets the field type. + * + * @return the field type + */ + public Class getFieldType() { + return fieldType; + } + + public Class getFieldSubType() { + return fieldSubType; + } + + /** + * A builder for the VectorStoreRecordField class. + * @param the type of the field + * @param the type of the builder + */ + public abstract static class Builder> + implements SemanticKernelBuilder { + + @Nullable + protected String name; + @Nullable + protected String storageName; + @Nullable + protected Class fieldType; + + @Nullable + protected Class fieldSubType; + + /** + * Sets the name of the field. + * + * @param name the name of the field + * @return the builder + */ + public U withName(String name) { + this.name = name; + return (U) this; + } + + /** + * Sets the storage name of the field. + * + * @param storageName the storage name of the field + * @return the builder + */ + public U withStorageName(String storageName) { + this.storageName = storageName; + return (U) this; + } + + /** + * Sets the field type. + * + * @param fieldType the field type + * @return the builder + */ + public U withFieldType(Class fieldType) { + this.fieldType = fieldType; + return (U) this; + } + + public U withFieldType(Class fieldType, Class fieldSubType) { + this.fieldType = fieldType; + this.fieldSubType = fieldSubType; + return (U) this; + } + + /** + * Builds the field. + * + * @return the field + */ + @Override + public abstract T build(); + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordKeyField.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordKeyField.java similarity index 51% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordKeyField.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordKeyField.java index 4502aa3b9..9e2a70e7e 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordKeyField.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordKeyField.java @@ -1,10 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recorddefinition; +package com.microsoft.semantickernel.data.vectorstorage.definition; /** * Represents the key field in a record. */ public class VectorStoreRecordKeyField extends VectorStoreRecordField { + + /** + * Create a builder for the VectorStoreRecordKeyField class. + * @return a new instance of the builder + */ public static Builder builder() { return new Builder(); } @@ -13,16 +18,27 @@ public static Builder builder() { * Creates a new instance of the VectorStoreRecordKeyField class. * * @param name the name of the field + * @param storageName the storage name of the field + * @param type the field type */ - public VectorStoreRecordKeyField(String name, String storageName) { - super(name, storageName); + public VectorStoreRecordKeyField(String name, String storageName, Class type) { + super(name, storageName, type); } + /** + * A builder for the VectorStoreRecordKeyField class. + */ public static class Builder extends VectorStoreRecordField.Builder { @Override public VectorStoreRecordKeyField build() { - return new VectorStoreRecordKeyField(name, storageName); + if (name == null) { + throw new IllegalArgumentException("name is required."); + } + if (fieldType == null) { + throw new IllegalArgumentException("fieldType is required."); + } + return new VectorStoreRecordKeyField(name, storageName, fieldType); } } } diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordVectorField.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordVectorField.java similarity index 77% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordVectorField.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordVectorField.java index 34653c840..b708d2fbb 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordVectorField.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/definition/VectorStoreRecordVectorField.java @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recorddefinition; +package com.microsoft.semantickernel.data.vectorstorage.definition; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -9,11 +9,13 @@ */ public class VectorStoreRecordVectorField extends VectorStoreRecordField { private final int dimensions; - @Nullable private final IndexKind indexKind; - @Nullable private final DistanceFunction distanceFunction; + /** + * Create a builder for the VectorStoreRecordVectorField class. + * @return a new instance of the builder + */ public static Builder builder() { return new Builder(); } @@ -23,6 +25,7 @@ public static Builder builder() { * * @param name the name of the field * @param storageName the storage name of the field + * @param fieldType the field type * @param dimensions the number of dimensions in the vector * @param indexKind the index kind * @param distanceFunction the distance function @@ -30,13 +33,16 @@ public static Builder builder() { public VectorStoreRecordVectorField( @Nonnull String name, @Nullable String storageName, + @Nonnull Class fieldType, + Class fieldSubType, int dimensions, @Nullable IndexKind indexKind, @Nullable DistanceFunction distanceFunction) { - super(name, storageName); + super(name, storageName, fieldType, fieldSubType); this.dimensions = dimensions; - this.indexKind = indexKind; - this.distanceFunction = distanceFunction; + this.indexKind = indexKind == null ? IndexKind.UNDEFINED : indexKind; + this.distanceFunction = distanceFunction == null ? DistanceFunction.UNDEFINED + : distanceFunction; } /** @@ -53,7 +59,6 @@ public int getDimensions() { * * @return the index kind */ - @Nullable public IndexKind getIndexKind() { return indexKind; } @@ -63,18 +68,18 @@ public IndexKind getIndexKind() { * * @return the distance function */ - @Nullable public DistanceFunction getDistanceFunction() { return distanceFunction; } + /** + * A builder for the VectorStoreRecordVectorField class. + */ public static class Builder extends VectorStoreRecordField.Builder { private int dimensions; - @Nullable - private IndexKind indexKind; - @Nullable - private DistanceFunction distanceFunction; + private IndexKind indexKind = IndexKind.UNDEFINED; + private DistanceFunction distanceFunction = DistanceFunction.UNDEFINED; /** * Sets the number of dimensions in the vector. @@ -119,11 +124,16 @@ public VectorStoreRecordVectorField build() { if (name == null) { throw new IllegalArgumentException("name is required"); } + if (fieldType == null) { + throw new IllegalArgumentException("fieldType is required"); + } if (dimensions <= 0) { throw new IllegalArgumentException("dimensions must be greater than 0"); } - return new VectorStoreRecordVectorField(name, storageName, dimensions, indexKind, + return new VectorStoreRecordVectorField(name, storageName, fieldType, fieldSubType, + dimensions, + indexKind, distanceFunction); } } diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/DeleteRecordOptions.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/DeleteRecordOptions.java similarity index 66% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/DeleteRecordOptions.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/DeleteRecordOptions.java index ad68a90f6..ed0dbb924 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/DeleteRecordOptions.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/DeleteRecordOptions.java @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recordoptions; +package com.microsoft.semantickernel.data.vectorstorage.options; /** * Options for deleting a record. diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/GetRecordOptions.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/GetRecordOptions.java new file mode 100644 index 000000000..74d2b0651 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/GetRecordOptions.java @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.options; + +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; + +/** + * Options for getting a record. + */ +public class GetRecordOptions { + + private final boolean includeVectors; + + private final boolean wildcardKeyMatching; + + /** + * Creates a new instance of the GetRecordOptions class. + * @param includeVectors A value indicating whether to include vectors in a response. + */ + public GetRecordOptions( + boolean includeVectors) { + this.includeVectors = includeVectors; + this.wildcardKeyMatching = false; + } + + /** + * Creates a new instance of the GetRecordOptions class. + * @param includeVectors A value indicating whether to include vectors in a response. + * @param wildcardKeyMatching A value indicating whether to use wildcard key matching. + */ + public GetRecordOptions( + boolean includeVectors, + boolean wildcardKeyMatching) { + this.includeVectors = includeVectors; + this.wildcardKeyMatching = wildcardKeyMatching; + } + + /** + * Gets whether to use wildcard key matching. + * @return {@code true} if wildcard key matching is used; otherwise, {@code false}. + */ + public boolean isWildcardKeyMatching() { + return wildcardKeyMatching; + } + + /** + * Creates a new builder. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for GetRecordOptions. + */ + public static class Builder implements SemanticKernelBuilder { + + private boolean includeVectors; + private boolean wildcardKeyMatching = false; + + /** + * Sets whether to include vectors. + * + * @param includeVectors whether to include vectors + * @return GetRecordOptions.Builder + */ + public Builder includeVectors(boolean includeVectors) { + this.includeVectors = includeVectors; + return this; + } + + /** + * Sets whether to use wildcard key matching. Default is false. Wildcard key matching allows + * for matching multiple ids, for instance using "LIKE 'a%'" on a SQL query. + *

+ * NOTE: Currently this is only supported by the SQL connectors. + * + * @param wildcardKeyMatching whether to use wildcard key matching + * @return GetRecordOptions.Builder + */ + public Builder setWildcardKeyMatching(boolean wildcardKeyMatching) { + this.wildcardKeyMatching = wildcardKeyMatching; + return this; + } + + /** + * Builds the options. + * + * @return GetRecordOptions + */ + @Override + public GetRecordOptions build() { + return new GetRecordOptions(includeVectors, wildcardKeyMatching); + } + } + + /** + * Gets whether to include vectors. + * + * @return whether to include vectors + */ + public boolean isIncludeVectors() { + return includeVectors; + } +} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/UpsertRecordOptions.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/UpsertRecordOptions.java similarity index 66% rename from semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/UpsertRecordOptions.java rename to semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/UpsertRecordOptions.java index 132eaf922..a0a9325b5 100644 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/UpsertRecordOptions.java +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/UpsertRecordOptions.java @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recordoptions; +package com.microsoft.semantickernel.data.vectorstorage.options; /** * Options for upserting a record. diff --git a/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/VectorSearchOptions.java b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/VectorSearchOptions.java new file mode 100644 index 000000000..53ef59dd0 --- /dev/null +++ b/semantickernel-api-data/src/main/java/com/microsoft/semantickernel/data/vectorstorage/options/VectorSearchOptions.java @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data.vectorstorage.options; + +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; + +import javax.annotation.Nullable; + +/** + * Options for a vector search. + */ +public class VectorSearchOptions { + + /** + * The default limit of the number of results to return. + */ + public static final int DEFAULT_TOP = 3; + + /** + * Creates a new instance of the VectorSearchOptions class with default values. + * + * @param vectorFieldName The name of the vector field. + * @return A new instance of the VectorSearchOptions class with default values. + */ + public static VectorSearchOptions createDefault(String vectorFieldName) { + return VectorSearchOptions.builder() + .withVectorFieldName(vectorFieldName).build(); + } + + @Nullable + private final VectorSearchFilter vectorSearchFilter; + @Nullable + private final String vectorFieldName; + private final int top; + private final int skip; + private final boolean includeVectors; + private final boolean includeTotalCount; + + /** + * Creates a new instance of the VectorSearchOptions class. + * @param vectorSearchFilter The vector search filter. + * @param vectorFieldName The name of the vector field. + * @param top The limit of the number of results to return. + * @param skip The offset of the results to return. + * @param includeVectors A value indicating whether to include vectors in the results. + * @param includeTotalCount A value indicating whether to include the total count of the results. + */ + public VectorSearchOptions(VectorSearchFilter vectorSearchFilter, + String vectorFieldName, int top, int skip, boolean includeVectors, + boolean includeTotalCount) { + this.vectorSearchFilter = vectorSearchFilter; + this.vectorFieldName = vectorFieldName; + this.top = Math.max(1, top); + this.skip = Math.max(0, skip); + this.includeVectors = includeVectors; + this.includeTotalCount = includeTotalCount; + } + + /** + * Gets the vector search filter. + * + * @return The vector search filter. + */ + @Nullable + public VectorSearchFilter getVectorSearchFilter() { + return vectorSearchFilter; + } + + /** + * Gets the name of the vector field. + * + * @return The name of the vector field. + */ + @Nullable + public String getVectorFieldName() { + return vectorFieldName; + } + + /** + * Gets the limit of the number of results to return. + * + * @return The limit of the number of results to return. + */ + public int getTop() { + return top; + } + + /** + * Gets the offset of the results to return. + * + * @return The offset of the results to return. + */ + public int getSkip() { + return skip; + } + + /** + * Gets a value indicating whether to include vectors in the results. + * + * @return A value indicating whether to include vectors in the results. + */ + public boolean isIncludeVectors() { + return includeVectors; + } + + /** + * Gets a value indicating whether to include the total count of the results. + * + * @return A value indicating whether to include the total count of the results. + */ + public boolean isIncludeTotalCount() { + return includeTotalCount; + } + + /** + * Creates a new instance of the Builder class. + * + * @return A new instance of the Builder class. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for the VectorSearchOptions class. + */ + public static class Builder implements SemanticKernelBuilder { + private VectorSearchFilter vectorSearchFilter; + private String vectorFieldName; + private int top = DEFAULT_TOP; + private int skip = 0; + private boolean includeVectors = false; + private boolean includeTotalCount = false; + + /** + * Sets the vector search filter. + * @param vectorSearchFilter the vector search filter + * @return {@code this} builder + */ + public Builder withVectorSearchFilter( + VectorSearchFilter vectorSearchFilter) { + this.vectorSearchFilter = vectorSearchFilter; + return this; + } + + /** + * Sets the name of the vector field. + * @param vectorFieldName the name of the vector field + * @return {@code this} builder + */ + public Builder withVectorFieldName(String vectorFieldName) { + this.vectorFieldName = vectorFieldName; + return this; + } + + /** + * Sets the limit of the number of results to return. + * @param top the limit of the number of results to return + * @return {@code this} builder + */ + public Builder withTop(int top) { + this.top = top; + return this; + } + + /** + * Sets the offset of the results to return. + * @param skip the offset of the results to return + * @return {@code this} builder + */ + public Builder withSkip(int skip) { + this.skip = skip; + return this; + } + + /** + * Sets a value indicating whether to include vectors in the results. + * @param includeVectors a value indicating whether to include vectors in the results + * @return {@code this} builder + */ + public Builder withIncludeVectors(boolean includeVectors) { + this.includeVectors = includeVectors; + return this; + } + + /** + * Sets a value indicating whether to include the total count of the results. + * @param includeTotalCount a value indicating whether to include the total count of the results + * @return {@code this} builder + */ + public Builder withIncludeTotalCount(boolean includeTotalCount) { + this.includeTotalCount = includeTotalCount; + return this; + } + + /** + * Builds a new instance of the VectorSearchOptions class. + * @return a new instance of the VectorSearchOptions class + */ + @Override + public VectorSearchOptions build() { + return new VectorSearchOptions(vectorSearchFilter, vectorFieldName, top, skip, + includeVectors, includeTotalCount); + } + } +} diff --git a/semantickernel-api-exceptions/pom.xml b/semantickernel-api-exceptions/pom.xml new file mode 100644 index 000000000..bb60deadb --- /dev/null +++ b/semantickernel-api-exceptions/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../pom.xml + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + Semantic Kernel Exceptions API + Defines the public interface for the Semantic Kernel Exceptions + + + + com.microsoft.semantic-kernel + semantickernel-api-localization + provided + + + com.google.code.findbugs + jsr305 + provided + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + 1 + + + + + + \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/AIException.java b/semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/AIException.java similarity index 68% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/AIException.java rename to semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/AIException.java index 17416a5b6..ac6290d6f 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/AIException.java +++ b/semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/AIException.java @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.exceptions; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -65,53 +66,57 @@ public enum ErrorCodes { /** * Unknown error. */ - UNKNOWN_ERROR("Unknown error"), + UNKNOWN_ERROR(SemanticKernelResources.getString("unknown.error")), /** * No response. */ - NO_RESPONSE("No response"), + NO_RESPONSE(SemanticKernelResources.getString("no.response")), /** * Access denied. */ - ACCESS_DENIED("Access is denied"), + ACCESS_DENIED(SemanticKernelResources.getString("access.is.denied")), /** * Invalid request. */ - INVALID_REQUEST("The request was invalid"), + INVALID_REQUEST(SemanticKernelResources.getString("the.request.was.invalid")), /** * Invalid response. */ - INVALID_RESPONSE_CONTENT("The content of the response was invalid"), + INVALID_RESPONSE_CONTENT( + SemanticKernelResources.getString("the.content.of.the.response.was.invalid")), /** * Throttling. */ - THROTTLING("The request was throttled"), + THROTTLING(SemanticKernelResources.getString("the.request.was.throttled")), /** * Request timeout. */ - REQUEST_TIMEOUT("The request timed out"), + REQUEST_TIMEOUT(SemanticKernelResources.getString("the.request.timed.out")), /** * Service error. */ - SERVICE_ERROR("There was an error in the service"), + SERVICE_ERROR(SemanticKernelResources.getString("there.was.an.error.in.the.service")), /** * Model not available. */ - MODEL_NOT_AVAILABLE("The requested model is not available"), + MODEL_NOT_AVAILABLE( + SemanticKernelResources.getString("the.requested.model.is.not.available")), /** * Invalid configuration. */ - INVALID_CONFIGURATION("The supplied configuration was invalid"), + INVALID_CONFIGURATION( + SemanticKernelResources.getString("the.supplied.configuration.was.invalid")), /** * Function type not supported. */ - FUNCTION_TYPE_NOT_SUPPORTED("The function is not supported"); + FUNCTION_TYPE_NOT_SUPPORTED( + SemanticKernelResources.getString("the.function.is.not.supported")); private final String message; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/ConfigurationException.java b/semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/ConfigurationException.java similarity index 80% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/ConfigurationException.java rename to semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/ConfigurationException.java index 1784ee824..e716f1fb7 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/ConfigurationException.java +++ b/semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/ConfigurationException.java @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.exceptions; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -65,27 +66,31 @@ public enum ErrorCodes { /** * Unknown error */ - UNKNOWN_ERROR("Unknown error"), + UNKNOWN_ERROR(SemanticKernelResources.getString("unknown.error")), /** * Could not find configuration file */ - CONFIGURATION_NOT_FOUND("Could not find configuration file"), + CONFIGURATION_NOT_FOUND( + SemanticKernelResources.getString("could.not.find.configuration.file")), /** * Could not parse or load configuration file */ - COULD_NOT_READ_CONFIGURATION("Could not parse or load configuration file"), + COULD_NOT_READ_CONFIGURATION( + SemanticKernelResources.getString("could.not.parse.or.load.configuration.file")), /** * Could not find any valid configuration settings */ - NO_VALID_CONFIGURATIONS_FOUND("Could not find any valid configuration settings"), + NO_VALID_CONFIGURATIONS_FOUND( + SemanticKernelResources.getString("could.not.find.any.valid.configuration.settings")), /** * Could not find value for configuration key */ - VALUE_NOT_FOUND("Could not find value for configuration key"); + VALUE_NOT_FOUND( + SemanticKernelResources.getString("could.not.find.value.for.configuration.key")); private final String message; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/SKCheckedException.java b/semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/SKCheckedException.java similarity index 73% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/SKCheckedException.java rename to semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/SKCheckedException.java index 7b8364ea5..073906920 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/SKCheckedException.java +++ b/semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/SKCheckedException.java @@ -37,32 +37,40 @@ public SKCheckedException(@Nullable String message, @Nullable Throwable cause) { super(message, cause); } - public SKCheckedException(Throwable e) { - super(e); + /** + * Initializes a new instance of the {@code SKCheckedException} class with its + * message set to {@code null} and the cause set to {@code e}. + * + * @param cause The exception that is the cause of the current exception. + */ + public SKCheckedException(Throwable cause) { + super(cause); } /** - * Forms a checked exception, if the exception is already an SK exception, it will be unwrapped + * Forms a checked exception, if the exception is already an SK exception, it + * will be unwrapped * and the cause extracted. * * @param message The message to be displayed - * @param e The exception to be thrown + * @param cause The exception that is the cause of the current exception. * @return A checked exception */ public static SKCheckedException build( String message, - @Nullable Exception e) { + @Nullable Exception cause) { - if (e == null) { + if (cause == null) { return new SKCheckedException(message); } - Throwable cause = e.getCause(); + Throwable wrappedCause = cause.getCause(); - if ((e instanceof SKCheckedException || e instanceof SKException) && cause != null) { - return new SKCheckedException(message, cause); + if ((cause instanceof SKCheckedException || cause instanceof SKException) + && wrappedCause != null) { + return new SKCheckedException(message, wrappedCause); } else { - return new SKCheckedException(message, e); + return new SKCheckedException(message, cause); } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/SKException.java b/semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/SKException.java similarity index 74% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/SKException.java rename to semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/SKException.java index 99aef0569..3f14a2f5c 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/exceptions/SKException.java +++ b/semantickernel-api-exceptions/src/main/java/com/microsoft/semantickernel/exceptions/SKException.java @@ -37,8 +37,14 @@ public SKException(@Nullable String message, @Nullable Throwable cause) { super(message, cause); } - public SKException(Throwable e) { - super(e); + /** + * Initializes a new instance of the {@code SKException} class with its + * message set to {@code null} and the cause set to {@code e}. + * + * @param cause The exception that is the cause of the current exception. + */ + public SKException(Throwable cause) { + super(cause); } /** @@ -46,23 +52,24 @@ public SKException(Throwable e) { * unwrapped and the cause extracted. * * @param message The message to be displayed - * @param e The exception to be thrown + * @param cause The exception that is the cause of the current exception. * @return An unchecked exception */ public static SKException build( String message, - @Nullable Exception e) { + @Nullable Exception cause) { - if (e == null) { + if (cause == null) { return new SKException(message); } - Throwable cause = e.getCause(); + Throwable wrappedCause = cause.getCause(); - if ((e instanceof SKCheckedException || e instanceof SKException) && cause != null) { - return new SKException(message, cause); + if ((cause instanceof SKCheckedException || cause instanceof SKException) + && wrappedCause != null) { + return new SKException(message, wrappedCause); } else { - return new SKException(message, e); + return new SKException(message, cause); } } diff --git a/semantickernel-api-localization/pom.xml b/semantickernel-api-localization/pom.xml new file mode 100644 index 000000000..b57ad77f1 --- /dev/null +++ b/semantickernel-api-localization/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../pom.xml + + + com.microsoft.semantic-kernel + semantickernel-api-localization + Semantic Kernel Localization API + Defines the public interface for the Semantic Kernel Localization + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + 1 + + + + + + \ No newline at end of file diff --git a/semantickernel-api-localization/src/main/java/com/microsoft/semantickernel/localization/SemanticKernelResources.java b/semantickernel-api-localization/src/main/java/com/microsoft/semantickernel/localization/SemanticKernelResources.java new file mode 100644 index 000000000..2ebc9c415 --- /dev/null +++ b/semantickernel-api-localization/src/main/java/com/microsoft/semantickernel/localization/SemanticKernelResources.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.localization; + +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +/** + * Provides access to the resources used by the Semantic Kernel. + */ +public class SemanticKernelResources { + + private static final String RESOURCE_BUNDLE_CLASS = "com.microsoft.semantickernel.localization.ResourceBundle"; + + private static ResourceBundle RESOURCE_BUNDLE; + private static Locale LOCALE; + + static { + LOCALE = setLocale(new Locale( + System.getProperty("semantickernel.locale", + String.valueOf(Locale.getDefault().getLanguage())))); + RESOURCE_BUNDLE = setResourceBundle(LOCALE); + } + + /** + * Load the localized resource bundle for the Semantic Kernel. + * If there is no resource bundle for the specified locale, the default + * resource bundle will be loaded. + * @param locale The locale to use. + * @return the resource bundle. + */ + public static ResourceBundle setResourceBundle(Locale locale) { + ResourceBundle resourceBundle; + try { + resourceBundle = PropertyResourceBundle.getBundle( + RESOURCE_BUNDLE_CLASS, locale); + } catch (MissingResourceException e) { + resourceBundle = PropertyResourceBundle.getBundle( + RESOURCE_BUNDLE_CLASS); + } + RESOURCE_BUNDLE = resourceBundle; + return resourceBundle; + } + + /** + * Set the locale for the Semantic Kernel. As a side effect, + * the localized resource bundle will be loaded. + * @param locale The locale to use. + * @return the locale. + */ + public static Locale setLocale(Locale locale) { + LOCALE = locale; + setResourceBundle(locale); + return locale; + } + + /** + * Get the string for the specified id from the resource bundle. + * @param id The id of the string. + * @param defaultValue The default value to return if the string is not found. + * @return the localized string, or the default value if the string is not found. + */ + public static String localize(String id, String defaultValue) { + if (RESOURCE_BUNDLE.containsKey(id)) { + return RESOURCE_BUNDLE.getString(id); + } else { + return defaultValue; + } + } + + /** + * Get the string for the specified id from the resource bundle. + * @param id The id of the string. + * @return the localized string, or the id if the string is not found. + */ + public static String getString(String id) { + return localize(id, id); + } + +} diff --git a/semantickernel-api-localization/src/main/resources/com/microsoft/semantickernel/localization/ResourceBundle.properties b/semantickernel-api-localization/src/main/resources/com/microsoft/semantickernel/localization/ResourceBundle.properties new file mode 100644 index 000000000..cf1d17b0b --- /dev/null +++ b/semantickernel-api-localization/src/main/resources/com/microsoft/semantickernel/localization/ResourceBundle.properties @@ -0,0 +1,57 @@ +a.named.argument.must.have.a.name=A named argument must have a name +a.named.argument.must.have.a.value=A named argument must have a value +a.value.must.be.defined.using.either.single.quotes.or.double.quotes.not.both=A value must be defined using either single quotes or double quotes, not both +a.value.must.have.single.quotes.or.double.quotes.on.both.sides=A value must have single quotes or double quotes on both sides +a.variable.must.start.with.the.symbol=A variable must start with the symbol {} +a.variable.must.start.with.the.symbol.and.have.a.name=A variable must start with the symbol {} and have a name +access.is.denied=Access is denied +annotation.on.method.is.requesting.a.string=Annotation on method: {} is requesting a String which is not assignable to method type {}, possibly as the type argument has not been provided on the annotation. +attempting.to.modify.function.after.it.has.already.been.subscribed=Attempting to modify function {}.{} after it has already been subscribed to. This is not necessarily an error but may be an unusual pattern and indicate a potential bug. +could.not.find.any.valid.configuration.settings=Could not find any valid configuration settings +could.not.find.configuration.file=Could not find configuration file +could.not.find.value.for.configuration.key=Could not find value for configuration key +could.not.parse.or.load.configuration.file=Could not parse or load configuration file +error.building.generative.model=Error building generative model. +error.generating.chat.completion=Error generating chat completion +error.parsing.prompt=Error parsing prompt +failed.to.load.file.0=Failed to load file: {0} +failed.to.parse.config.file=Failed to parse config file {} +failed.to.read.file=Failed to read file +failed.to.read.file1=Failed to read file {} +for.the.function.0.1.the.unknown.parameter.name.was.detected=For the function {0}.{1}, the unknown parameter name was detected as "{2}" this is argument number {3} to the function, this indicates that the argument name for this function was removed during compilation and semantic-kernel is unable to determine the name of the parameter. To support this function the argument must be annotated with @SKFunctionParameters or @SKFunctionInputAttribute. Alternatively the function was invoked with a required context variable missing and no default value. +function.has.already.been.subscribed.to.this.is.not.necessarily.an.error.but.may.be.an.unusual.pattern=Function {}.{} has already been subscribed to. This is not necessarily an error but may be an unusual pattern. +functions.only.support.named.arguments.after.the.first.argument=Functions only support named arguments after the first argument. Argument {} is not named. +invalid.block.0=Invalid block{0} +no.config.for.in=No config for {} in {} +no.converter.found.for.to=No converter found for {} to {} +no.functions.found.in.class.this.can.be.caused.by=No functions found in class {}. This can be caused by DI frameworks that create proxies, or modules that are not making your methods visible. Try using: KernelPluginFactory.createFromObject(Class clazz, Object target, String pluginName). +no.response=No response +no.service.found.meeting.requirements=No service found meeting requirements +no.variable.type.explicitly.specified.by.calling.withresulttype.for.function=No variable type explicitly specified by calling 'withResultType' for function invocation: {}.{}. This may cause a runtime error (probably a ClassCastException) if the result type is not compatible with the expected type. +plugin.already.exists.overwriting.existing.plugin=Plugin {} already exists, overwriting existing plugin +rendered.prompt=RENDERED PROMPT: \n{} +requested.a.non.existent.service.type.of.consider.requesting.a.textaiservice.instead=Requested a non-existent service type of {}. Consider requesting a TextAIService instead. +something.went.wrong.while.rendering.the.semantic.function.or.while.executing.the.text.completion.function.error=Something went wrong while rendering the semantic function or while executing the text completion. Function: {}.{}. Error: {} +syntax.error.the.template.syntax.used.is.not.valid=Syntax error, the template syntax used is not valid +the.block.type.produced.be.the.tokenizer.was.not.expected=The block type produced be the tokenizer was not expected +the.content.of.the.response.was.invalid=The content of the response was invalid +the.first.arg.of.a.function.must.be.a.quoted.string.variable.or.named.argument=The first arg of a function must be a quoted string, variable or named argument +the.function.is.not.supported=The function is not supported +the.request.timed.out=The request timed out +the.request.was.invalid=The request was invalid +the.request.was.throttled=The request was throttled +the.requested.model.is.not.available=The requested model is not available +the.supplied.configuration.was.invalid=The supplied configuration was invalid +the.template.execution.failed.e.g.a.function.call.threw.an.exception=The template execution failed, e.g. a function call threw an exception +the.template.requires.an.unknown.function=The template requires an unknown function +the.variable.name.contains.invalid.characters.only.alphanumeric.chars.and.underscore.are.allowed=The variable name '{}' contains invalid characters. Only alphanumeric chars and underscore are allowed. +the.variable.name.is.empty=The variable name is empty +there.was.an.error.in.the.service=There was an error in the service +there.was.an.issue.with.the.named.argument.value.for=There was an issue with the named argument value for {} +this.error.indicates.that.you.have.attempted.to.use.a.chat.completion.model=This error indicates that you have attempted to use a chat completion model in a text completion service. Try using a chat completion service instead when building your kernel, for instance when building your service use SKBuilders.chatCompletion() rather than SKBuilders.textCompletionService(). +unable.to.load.prompt.template.config.for.in=Unable to load prompt template config for {} in {} +unable.to.load.service.s=Unable to load service %s +unexpected.named.argument.found.expected.function.name.first=Unexpected named argument found. Expected function name first. +unexpected.second.token.found.0=Unexpected second token found: {0} +unknown.error=Unknown error +variable.not.found=Variable `{}{}` not found \ No newline at end of file diff --git a/semantickernel-api-textembedding-services/pom.xml b/semantickernel-api-textembedding-services/pom.xml new file mode 100644 index 000000000..3cba6dd41 --- /dev/null +++ b/semantickernel-api-textembedding-services/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.5.1-SNAPSHOT + ../pom.xml + + + com.microsoft.semantic-kernel + semantickernel-api-textembedding-services + Semantic Kernel Services API + Defines the public interface for the Semantic Kernel Services + + + + io.projectreactor + reactor-core + + + com.google.code.findbugs + jsr305 + provided + + + com.github.spotbugs + spotbugs-annotations + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + 1 + + + + + + \ No newline at end of file diff --git a/semantickernel-api-textembedding-services/src/main/java/com/microsoft/semantickernel/services/textembedding/Embedding.java b/semantickernel-api-textembedding-services/src/main/java/com/microsoft/semantickernel/services/textembedding/Embedding.java new file mode 100644 index 000000000..e98941f6c --- /dev/null +++ b/semantickernel-api-textembedding-services/src/main/java/com/microsoft/semantickernel/services/textembedding/Embedding.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.services.textembedding; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Represents a strongly typed vector of numeric data. */ +@SuppressFBWarnings("SING_SINGLETON_HAS_NONPRIVATE_CONSTRUCTOR") // This class is not a singleton +public class Embedding { + + // vector is immutable! + private final List vector; + + private static final Embedding EMPTY = new Embedding(); + + /** + * Returns an empty {@code Embedding} instance. + * @return An empty {@code Embedding} instance. + */ + public static Embedding empty() { + return EMPTY; + } + + /** Initializes a new instance of the Embedding class. */ + public Embedding() { + this.vector = Collections.emptyList(); + } + + /** + * Initializes a new instance of the Embedding class that contains numeric elements copied from + * the specified collection + * + * @param vector The collection whose elements are copied to the new Embedding + */ + public Embedding(@Nonnull List vector) { + Objects.requireNonNull(vector); + this.vector = Collections.unmodifiableList(vector); + } + + /** + * Initializes a new instance of the Embedding class that contains numeric elements copied from + * the specified array + * + * @param vector The array whose elements are copied to the new Embedding + */ + public Embedding(@Nonnull float[] vector) { + Objects.requireNonNull(vector); + List list = new ArrayList<>(vector.length); + for (float f : vector) { + list.add(f); + } + this.vector = Collections.unmodifiableList(list); + } + + /** + * Return the embedding vector as a read-only list. + * + * @return The embedding vector as a read-only list. + */ + public List getVector() { + return Collections.unmodifiableList(this.vector); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/EmbeddingGenerationService.java b/semantickernel-api-textembedding-services/src/main/java/com/microsoft/semantickernel/services/textembedding/EmbeddingGenerationService.java similarity index 61% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/EmbeddingGenerationService.java rename to semantickernel-api-textembedding-services/src/main/java/com/microsoft/semantickernel/services/textembedding/EmbeddingGenerationService.java index 86d4d27dd..561dbe1d7 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/EmbeddingGenerationService.java +++ b/semantickernel-api-textembedding-services/src/main/java/com/microsoft/semantickernel/services/textembedding/EmbeddingGenerationService.java @@ -1,14 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.services.textembedding; -import com.microsoft.semantickernel.builders.SemanticKernelBuilder; import com.microsoft.semantickernel.services.AIService; -import reactor.core.publisher.Mono; - import java.util.List; +import reactor.core.publisher.Mono; -/** Interface for text embedding generation services */ +/** + * Interface for text embedding generation services + * @param The type of the data to generate embeddings for + */ public interface EmbeddingGenerationService extends AIService { + /** * Generates a list of embeddings associated to the data * @@ -16,4 +18,14 @@ public interface EmbeddingGenerationService extends AIService { * @return List of embeddings of each data point */ Mono> generateEmbeddingsAsync(List data); + + /** + * Generates an embedding associated to the data + * + * @param data Text to generate embedding for + * @return Embedding of the data + */ + + Mono generateEmbeddingAsync(TValue data); + } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/TextEmbeddingGenerationService.java b/semantickernel-api-textembedding-services/src/main/java/com/microsoft/semantickernel/services/textembedding/TextEmbeddingGenerationService.java similarity index 100% rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/TextEmbeddingGenerationService.java rename to semantickernel-api-textembedding-services/src/main/java/com/microsoft/semantickernel/services/textembedding/TextEmbeddingGenerationService.java diff --git a/semantickernel-api/pom.xml b/semantickernel-api/pom.xml index 6171818b2..dcebfbc60 100644 --- a/semantickernel-api/pom.xml +++ b/semantickernel-api/pom.xml @@ -6,7 +6,7 @@ com.microsoft.semantic-kernel semantickernel-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT ../pom.xml @@ -15,6 +15,35 @@ Semantic Kernel API Defines the public interface for the Semantic Kernel + + com.microsoft.semantic-kernel + semantickernel-api-data + + + com.microsoft.semantic-kernel + semantickernel-api-exceptions + + + com.microsoft.semantic-kernel + semantickernel-api-builders + + + com.microsoft.semantic-kernel + semantickernel-api-localization + + + com.microsoft.semantic-kernel + semantickernel-api-textembedding-services + + + com.microsoft.semantic-kernel + semantickernel-api-ai-services + + + io.opentelemetry.instrumentation + opentelemetry-reactor-3.1 + 2.26.1-alpha + com.azure azure-ai-openai @@ -67,7 +96,6 @@ org.wiremock wiremock - 3.3.1 test @@ -80,7 +108,6 @@ org.mockito mockito-junit-jupiter - 5.11.0 test @@ -95,12 +122,49 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 false 1 + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack-dependencies + + unpack-dependencies + + + + semantickernel-api-exceptions,semantickernel-api-builders,semantickernel-api-localization,semantickernel-api-textembedding-services,semantickernel-api-ai-services,semantickernel-api-data + + ${project.build.directory}/lib + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + src/assembly/custom.xml + + false + + + + make-assembly + package + + single + + + + diff --git a/semantickernel-api/src/assembly/custom.xml b/semantickernel-api/src/assembly/custom.xml new file mode 100644 index 000000000..48becfffa --- /dev/null +++ b/semantickernel-api/src/assembly/custom.xml @@ -0,0 +1,22 @@ + + with-exceptions + + jar + + false + + + ${project.build.outputDirectory} + / + + + ${project.build.directory}/lib + / + + **/** + + + + \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/Kernel.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/Kernel.java index 4ab1553fa..5e6cc2fdf 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/Kernel.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/Kernel.java @@ -9,7 +9,7 @@ import com.microsoft.semantickernel.orchestration.InvocationContext; import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.AIService; import com.microsoft.semantickernel.services.AIServiceCollection; import com.microsoft.semantickernel.services.AIServiceSelection; @@ -163,16 +163,36 @@ public FunctionInvocation invokePromptAsync(@Nonnull String prompt) { return invokeAsync(KernelFunction.createFromPrompt(prompt).build()); } + /** + * Invokes a Prompt. + * + * @param The return type of the prompt. + * @param prompt The prompt to invoke. + * @param arguments The arguments to pass to the prompt. + * @return The result of the prompt invocation. + * @see KernelFunction#invokeAsync(Kernel) + */ public FunctionInvocation invokePromptAsync(@Nonnull String prompt, - @Nonnull KernelFunctionArguments arguments) { + @Nonnull KernelArguments arguments) { KernelFunction function = KernelFunction.createFromPrompt(prompt).build(); return function.invokeAsync(this) .withArguments(arguments); } + /** + * Invokes a Prompt. + * + * @param The return type of the prompt. + * @param prompt The prompt to invoke. + * @param arguments The arguments to pass to the prompt. + * @param invocationContext Additional context to used when invoking the prompt. + * @return The result of the prompt invocation. + * @see KernelFunction#invokeAsync(Kernel) + */ + public FunctionInvocation invokePromptAsync(@Nonnull String prompt, - @Nonnull KernelFunctionArguments arguments, @Nonnull InvocationContext invocationContext) { + @Nonnull KernelArguments arguments, @Nonnull InvocationContext invocationContext) { KernelFunction function = KernelFunction.createFromPrompt(prompt).build(); @@ -258,7 +278,7 @@ public List> getFunctions() { * addition to any hooks provided to a function. * * @return The {@code KernelHooks} used throughout the kernel. - * @see KernelFunction#invokeAsync(Kernel, KernelFunctionArguments, ContextVariableType, + * @see KernelFunction#invokeAsync(Kernel, KernelArguments, ContextVariableType, * InvocationContext) */ @SuppressFBWarnings("EI_EXPOSE_REP") @@ -282,14 +302,12 @@ public AIServiceSelector getServiceSelector() { * @param clazz The class of the service to get. * @return The service of the specified type from the kernel. * @throws ServiceNotFoundException if the service is not found. - * @see com.microsoft.semantickernel.services.AIServiceSelector#trySelectAIService(Class, - * KernelFunction, com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments) + * @see com.microsoft.semantickernel.services.AIServiceSelector#trySelectAIService(Class, KernelArguments) */ public T getService(Class clazz) throws ServiceNotFoundException { AIServiceSelection selector = serviceSelector .trySelectAIService( clazz, - null, null); if (selector == null) { @@ -299,6 +317,30 @@ public T getService(Class clazz) throws ServiceNotFound return selector.getService(); } + /** + * Get the service of the specified type from the kernel. + * + * @param The type of the service to get. + * @param clazz The class of the service to get. + * @param args The arguments to help select the service to get. + * @return The service of the specified type from the kernel. + * @throws ServiceNotFoundException if the service is not found. + * @see com.microsoft.semantickernel.services.AIServiceSelector#trySelectAIService(Class, KernelArguments) + */ + public T getService(Class clazz, KernelArguments args) + throws ServiceNotFoundException { + AIServiceSelection selector = serviceSelector + .trySelectAIService( + clazz, + args); + + if (selector == null) { + throw new ServiceNotFoundException("Unable to find service of type " + clazz.getName()); + } + + return selector.getService(); + } + /** * A fluent builder for creating a new instance of {@code Kernel}. */ diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelPluginCollection.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelPluginCollection.java index 851934d42..12d4e50ad 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelPluginCollection.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelPluginCollection.java @@ -3,6 +3,7 @@ import com.microsoft.semantickernel.contextvariables.CaseInsensitiveMap; import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionMetadata; @@ -122,7 +123,8 @@ KernelPlugin getPlugin(String pluginName) { */ void add(KernelPlugin plugin) { if (plugins.containsKey(plugin.getName())) { - LOGGER.warn("Plugin {} already exists, overwriting existing plugin", plugin.getName()); + LOGGER.warn(SemanticKernelResources.getString( + "plugin.already.exists.overwriting.existing.plugin"), plugin.getName()); } plugins.put(plugin.getName(), plugin); diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/Agent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/Agent.java new file mode 100644 index 000000000..f69b2152d --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/Agent.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.agents; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.annotation.Nullable; + +/** + * Interface for a semantic kernel agent. + */ +public interface Agent { + + /** + * Gets the agent's ID. + * + * @return The agent's ID + */ + String getId(); + + /** + * Gets the agent's name. + * + * @return The agent's name + */ + String getName(); + + /** + * Gets the agent's description. + * + * @return The agent's description + */ + String getDescription(); + + /** + * Invokes the agent with the given message. + * + * @param message The message to process + * @return A Mono containing the agent response + */ + Mono>>> invokeAsync( + @Nullable ChatMessageContent message); + + /** + * Invokes the agent with the given message and thread. + * + * @param message The message to process + * @param thread The agent thread to use + * @return A Mono containing the agent response + */ + Mono>>> invokeAsync( + @Nullable ChatMessageContent message, + @Nullable AgentThread thread); + + /** + * Invokes the agent with the given message, thread, and options. + * + * @param message The message to process + * @param thread The agent thread to use + * @param options The options for invoking the agent + * @return A Mono containing the agent response + */ + Mono>>> invokeAsync( + @Nullable ChatMessageContent message, + @Nullable AgentThread thread, + @Nullable AgentInvokeOptions options); + + /** + * Invoke the agent with the given chat history. + * + * @param messages The chat history to process + * @param thread The agent thread to use + * @param options The options for invoking the agent + * @return A Mono containing the agent response + */ + Mono>>> invokeAsync( + List> messages, + @Nullable AgentThread thread, + @Nullable AgentInvokeOptions options); + + /** + * Notifies the agent of a new message. + * + * @param thread The agent thread to use + */ + Mono notifyThreadOfNewMessageAsync(AgentThread thread, ChatMessageContent newMessage); +} \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentInvokeOptions.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentInvokeOptions.java new file mode 100644 index 000000000..6b6d57ed3 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentInvokeOptions.java @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.agents; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import javax.annotation.Nullable; + +/** + * Options for invoking an agent. + */ +public class AgentInvokeOptions { + + @Nullable + private final KernelArguments kernelArguments; + @Nullable + private final Kernel kernel; + @Nullable + private final String additionalInstructions; + @Nullable + private final InvocationContext invocationContext; + + /** + * Default constructor for AgentInvokeOptions. + */ + public AgentInvokeOptions() { + this(null, null, null, null); + } + + /** + * Constructor for AgentInvokeOptions. + * + * @param kernelArguments The arguments for the kernel function. + * @param kernel The kernel to use. + * @param additionalInstructions Additional instructions for the agent. + * @param invocationContext The invocation context. + */ + public AgentInvokeOptions(@Nullable KernelArguments kernelArguments, + @Nullable Kernel kernel, + @Nullable String additionalInstructions, + @Nullable InvocationContext invocationContext) { + this.kernelArguments = kernelArguments != null ? kernelArguments.copy() : null; + this.kernel = kernel; + this.additionalInstructions = additionalInstructions; + this.invocationContext = invocationContext; + } + + /** + * Get the kernel arguments. + * + * @return The kernel arguments. + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + public KernelArguments getKernelArguments() { + return kernelArguments; + } + + /** + * Get the kernel. + * + * @return The kernel. + */ + public Kernel getKernel() { + return kernel; + } + + /** + * Get additional instructions. + * + * @return The additional instructions. + */ + public String getAdditionalInstructions() { + return additionalInstructions; + } + + /** + * Get the invocation context. + * + * @return The invocation context. + */ + public InvocationContext getInvocationContext() { + return invocationContext; + } + + /** + * Builder for AgentInvokeOptions. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder implements SemanticKernelBuilder { + + private KernelArguments kernelArguments; + private Kernel kernel; + private String additionalInstructions; + private InvocationContext invocationContext; + + /** + * Set the kernel arguments. + * + * @param kernelArguments The kernel arguments. + * @return The builder. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withKernelArguments(KernelArguments kernelArguments) { + this.kernelArguments = kernelArguments; + return this; + } + + /** + * Set the kernel. + * + * @param kernel The kernel. + * @return The builder. + */ + public Builder withKernel(Kernel kernel) { + this.kernel = kernel; + return this; + } + + /** + * Set additional instructions. + * + * @param additionalInstructions The additional instructions. + * @return The builder. + */ + public Builder withAdditionalInstructions(String additionalInstructions) { + this.additionalInstructions = additionalInstructions; + return this; + } + + /** + * Set the invocation context. + * + * @param invocationContext The invocation context. + * @return The builder. + */ + public Builder withInvocationContext(InvocationContext invocationContext) { + this.invocationContext = invocationContext; + return this; + } + + /** + * Build the object. + * + * @return a constructed object. + */ + @Override + public AgentInvokeOptions build() { + return new AgentInvokeOptions( + kernelArguments, + kernel, + additionalInstructions, + invocationContext); + } + } +} \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentResponseItem.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentResponseItem.java new file mode 100644 index 000000000..0b4550986 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentResponseItem.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.agents; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class AgentResponseItem { + private final T message; + private final AgentThread thread; + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public AgentResponseItem(T message, AgentThread thread) { + this.message = message; + this.thread = thread; + } + + /** + * Gets the agent response message. + * + * @return The message. + */ + public T getMessage() { + return message; + } + + /** + * Gets the thread. + * + * @return The thread. + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + public AgentThread getThread() { + return thread; + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentThread.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentThread.java new file mode 100644 index 000000000..94538f41a --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentThread.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.agents; + +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import reactor.core.publisher.Mono; + +/** + * Interface for an agent thread. + */ +public interface AgentThread { + /** + * Get the thread ID. + * + * @return The thread ID. + */ + String getId(); + + /** + * Create a new thread. + * + * @return A Mono containing the thread ID. + */ + Mono createAsync(); + + /** + * Delete the thread. + * + * @return A Mono indicating completion. + */ + Mono deleteAsync(); + + /** + * Check if the thread is deleted. + * + * @return A Mono containing true if the thread is deleted, false otherwise. + */ + boolean isDeleted(); + + /** + * Create a copy of the thread. + * + * @return A new instance of the thread. + */ + AgentThread copy(); + + /** + * Handle a new message in the thread. + * + * @param newMessage The new message to handle. + * @return A Mono indicating completion. + */ + Mono onNewMessageAsync(ChatMessageContent newMessage); +} \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/BaseAgentThread.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/BaseAgentThread.java new file mode 100644 index 000000000..c66fe9b90 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/BaseAgentThread.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.agents; + +public abstract class BaseAgentThread implements AgentThread { + + protected String id; + protected boolean isDeleted; + + public BaseAgentThread() { + } + + public BaseAgentThread(String id) { + this.id = id; + } + + @Override + public String getId() { + return id; + } + + @Override + public boolean isDeleted() { + return isDeleted; + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/KernelAgent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/KernelAgent.java new file mode 100644 index 000000000..8403093eb --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/KernelAgent.java @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.agents; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.InvocationReturnMode; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; + +public abstract class KernelAgent implements Agent { + + protected final String id; + protected final String name; + protected final String description; + protected final Kernel kernel; + protected final KernelArguments kernelArguments; + protected final InvocationContext invocationContext; + protected final String instructions; + protected final PromptTemplate template; + + protected KernelAgent( + String id, + String name, + String description, + Kernel kernel, + KernelArguments kernelArguments, + InvocationContext invocationContext, + String instructions, + PromptTemplate template) { + this.id = id != null ? id : UUID.randomUUID().toString(); + this.name = name; + this.description = description; + this.kernel = kernel; + this.kernelArguments = kernelArguments != null + ? kernelArguments.copy() + : KernelArguments.builder().build(); + this.invocationContext = invocationContext != null + ? invocationContext + : InvocationContext.builder().build(); + this.instructions = instructions; + this.template = template; + } + + /** + * Gets the agent's ID. + * + * @return The agent's ID + */ + public String getId() { + return id; + } + + /** + * Gets the agent's name. + * + * @return The agent's name + */ + public String getName() { + return name; + } + + /** + * Gets the agent's description. + * + * @return The agent's description + */ + public String getDescription() { + return description; + } + + /** + * Gets the kernel used by the agent. + * + * @return The kernel used by the agent + */ + public Kernel getKernel() { + return kernel; + } + + /** + * Gets the invocation context used by the agent. + * + * @return The invocation context used by the agent + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + public KernelArguments getKernelArguments() { + return kernelArguments; + } + + /** + * Gets the invocation context used by the agent. + * + * @return The invocation context used by the agent + */ + public String getInstructions() { + return instructions; + } + + /** + * Gets the invocation context used by the agent. + * + * @return The invocation context used by the agent + */ + public PromptTemplate getTemplate() { + return template; + } + + /** + * Merges the provided arguments with the current arguments. + * Provided arguments will override the current arguments. + * + * @param arguments The arguments to merge with the current arguments. + */ + protected KernelArguments mergeArguments(KernelArguments arguments) { + if (arguments == null) { + return kernelArguments; + } + + Map executionSettings = new HashMap<>( + kernelArguments.getExecutionSettings()); + executionSettings.putAll(arguments.getExecutionSettings()); + + return KernelArguments.builder() + .withVariables(kernelArguments) + .withVariables(arguments) + .withExecutionSettings(executionSettings) + .build(); + } + + /** + * Formats the instructions using the provided kernel, arguments, and context. + * + * @param kernel The kernel to use for formatting. + * @param arguments The arguments to use for formatting. + * @param context The context to use for formatting. + * @return A Mono that resolves to the formatted instructions. + */ + protected Mono renderInstructionsAsync(Kernel kernel, KernelArguments arguments, + InvocationContext context) { + if (template != null) { + return template.renderAsync(kernel, arguments, context); + } else { + return Mono.just(instructions); + } + } + + protected Mono ensureThreadExistsWithMessagesAsync( + List> messages, AgentThread thread, Supplier threadSupplier) { + return Mono.defer(() -> { + // Check if the thread already exists + // If it does, we can work with a copy of it + AgentThread newThread = thread == null ? threadSupplier.get() : thread.copy(); + + return newThread.createAsync() + .thenMany(Flux.fromIterable(messages)) + .concatMap(message -> { + return notifyThreadOfNewMessageAsync(newThread, message) + .then(Mono.just(message)); + }) + .then(Mono.just((T) newThread)); + }); + } + + @Override + public Mono>>> invokeAsync( + @Nullable ChatMessageContent message) { + return invokeAsync(message, null, null); + } + + @Override + public Mono>>> invokeAsync( + @Nullable ChatMessageContent message, + @Nullable AgentThread thread) { + return invokeAsync(message, thread, null); + } + + @Override + public Mono>>> invokeAsync( + @Nullable ChatMessageContent message, + @Nullable AgentThread thread, + @Nullable AgentInvokeOptions options) { + ArrayList> messages = new ArrayList<>(); + if (message != null) { + messages.add(message); + } + return invokeAsync(messages, thread, options); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contents/FunctionCallContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contents/FunctionCallContent.java new file mode 100644 index 000000000..8c973ab01 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contents/FunctionCallContent.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.contents; + +import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.services.KernelContentImpl; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents the content of a function call. + *

+ * This class is used to represent a function call in the context of a chat message. + */ +public class FunctionCallContent extends KernelContentImpl { + + @Nullable + private final String id; + @Nullable + private final String pluginName; + private final String functionName; + @Nullable + private final KernelArguments arguments; + + /** + * Creates a new instance of the {@link FunctionCallContent} class. + * + * @param functionName The name of the function. + * @param pluginName The name of the plugin with which this function is associated, if any. + * @param id The ID of the tool call. + * @param arguments A name/value collection of the arguments to the function, if any. + */ + public FunctionCallContent( + String functionName, + @Nullable String pluginName, + @Nullable String id, + @Nullable KernelArguments arguments) { + this.functionName = functionName; + this.pluginName = pluginName; + this.id = id; + if (arguments == null) { + this.arguments = null; + } else { + this.arguments = arguments.copy(); + } + } + + /** + * Gets the ID of the tool call. + * + * @return The ID of the tool call. + */ + @Nullable + public String getId() { + return id; + } + + /** + * Gets the name of the plugin with which this function is associated, if any. + * + * @return The name of the plugin with which this function is associated, if any. + */ + @Nullable + public String getPluginName() { + return pluginName; + } + + /** + * Gets the name of the function. + * + * @return The name of the function. + */ + public String getFunctionName() { + return functionName; + } + + /** + * Gets a name/value collection of the arguments to the function, if any. + * + * @return A name/value collection of the arguments to the function, if any. + */ + @Nullable + public KernelArguments getArguments() { + if (arguments == null) { + return null; + } + return arguments.copy(); + } + + /** + * Gets list of function calls from the message content. + * + * @param messageContent The message content. + * @return The function calls. + */ + public static List getFunctionCalls(ChatMessageContent messageContent) { + if (messageContent.getItems() == null) { + return null; + } + + return messageContent.getItems().stream().filter( + item -> item instanceof FunctionCallContent) + .map(item -> (FunctionCallContent) item) + .collect(Collectors.toList()); + } + + /** + * Gets the content returned by the AI service. + * + * @return The content. + */ + @Nullable + @Override + public String getContent() { + return null; + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariable.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariable.java index f749974e6..5e79b872d 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariable.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariable.java @@ -128,7 +128,7 @@ public static ContextVariable convert( if (typeOfActualReturnedType != null) { // Try the to object T converted = typeOfActualReturnedType.getConverter().toObject(contextVariableTypes, it, - requestedResultType); + requestedResultType, false); if (converted != null) { return contextVariableTypes.getVariableTypeForClass(requestedResultType) diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypeConverter.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypeConverter.java index 687ef302e..3082ecf8a 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypeConverter.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypeConverter.java @@ -2,7 +2,7 @@ package com.microsoft.semantickernel.contextvariables; import com.microsoft.semantickernel.exceptions.SKException; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -24,8 +24,19 @@ public class ContextVariableTypeConverter { private static final Logger LOGGER = LoggerFactory.getLogger( ContextVariableTypeConverter.class); + /** + * A function to convert ContextVariable to a prompt string. + * @param the type of ContextVariable to convert + */ public interface ToPromptStringFunction { + /** + * Convert the type to a prompt string. + * + * @param types the context variable types + * @param t the type to convert + * @return the prompt string + */ String toPromptString(ContextVariableTypes types, T t); } @@ -115,6 +126,7 @@ public ContextVariableTypeConverter( * Use this converter to convert the object to the type of the context variable. * * @param the type to convert to + * @param types the context variable types * @param t the object to convert * @param clazz the class of the type to convert to * @return the converted object @@ -122,6 +134,23 @@ public ContextVariableTypeConverter( @Nullable @SuppressWarnings("unchecked") public U toObject(ContextVariableTypes types, @Nullable Object t, Class clazz) { + return toObject(types, t, clazz, true); + } + + /** + * Use this converter to convert the object to the type of the context variable. + * + * @param types the context variable types + * @param t the object to convert + * @param clazz the class of the type to convert to + * @param logWarnings whether to log warnings + * @param the type to convert to + * @return the converted object + */ + @Nullable + @SuppressWarnings("unchecked") + public U toObject(ContextVariableTypes types, @Nullable Object t, Class clazz, + boolean logWarnings) { if (t == null) { return null; } @@ -144,7 +173,11 @@ public U toObject(ContextVariableTypes types, @Nullable Object t, Class c return (U) converter.get().toObject((T) t); } - LOGGER.warn("No converter found for {} to {}", t.getClass(), clazz); + if (logWarnings) { + LOGGER.warn(SemanticKernelResources.getString("no.converter.found.for.to"), + t.getClass(), + clazz); + } return null; } @@ -171,7 +204,7 @@ public T fromObject(@Nullable Object s) { * constructor. * * @param types the context variable types, if {@code null} the global types are used - * @param t the type to convert + * @param t the type to convert * @return the prompt string */ public String toPromptString(@Nullable ContextVariableTypes types, @Nullable T t) { @@ -293,6 +326,7 @@ public static Builder builder(Class clazz) { /** * A builder for a context variable type converter. + * @param the type of the context variable */ public static class Builder { @@ -306,7 +340,6 @@ public static class Builder { * * @param clazz the class of the type */ - @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") public Builder(Class clazz) { this.clazz = clazz; fromObject = x -> ContextVariableTypes.convert(x, clazz); diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypes.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypes.java index 21e12e70a..dd12bb706 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypes.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypes.java @@ -94,6 +94,14 @@ public ContextVariableTypes() { variableTypes = new HashMap<>(); } + /** + * Get the globally available collectors, which is the + * default collection of context variable types and those that + * have been added with + * {@link #addGlobalConverter(ContextVariableTypeConverter)}. + * + * @return The collection of globally available converters. + */ public static ContextVariableTypes getGlobalTypes() { return new ContextVariableTypes(DEFAULT_TYPES); } @@ -104,7 +112,11 @@ public static ContextVariableTypes getGlobalTypes() { * @param contextVariableTypes The collection of context variable types to copy. */ public ContextVariableTypes(ContextVariableTypes contextVariableTypes) { - this.variableTypes = new HashMap<>(contextVariableTypes.variableTypes); + if (contextVariableTypes != null) { + this.variableTypes = new HashMap<>(contextVariableTypes.variableTypes); + } else { + this.variableTypes = new HashMap<>(); + } } /** diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/CollectionVariableContextVariableTypeConverter.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/CollectionVariableContextVariableTypeConverter.java index 5a5c14cf5..5b2661971 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/CollectionVariableContextVariableTypeConverter.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/CollectionVariableContextVariableTypeConverter.java @@ -7,7 +7,6 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableType; import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.Collection; import java.util.stream.Collectors; @@ -15,13 +14,15 @@ * A {@link ContextVariableTypeConverter} for {@code java.util.Collection} variables. Use * {@code ContextVariableTypes.getGlobalVariableTypeForClass(String.class)} to get an instance of * this class. - * * @see ContextVariableTypes#getGlobalVariableTypeForClass(Class) */ public class CollectionVariableContextVariableTypeConverter extends ContextVariableTypeConverter { - @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") + /** + * Creates a new instance of the {@link CollectionVariableContextVariableTypeConverter} class. + * @param delimiter The delimiter to use joining elements of the collection. + */ public CollectionVariableContextVariableTypeConverter(String delimiter) { super( Collection.class, @@ -35,12 +36,15 @@ public CollectionVariableContextVariableTypeConverter(String delimiter) { /** * Creates a new instance of the {@link CollectionVariableContextVariableTypeConverter} class. */ - - @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") public CollectionVariableContextVariableTypeConverter() { this(","); } + /** + * Gets a function that converts a collection to a string. + * @param delimiter The delimiter to use joining elements of the collection. + * @return A function that converts a collection to a string. + */ @SuppressWarnings("NullAway") public static ToPromptStringFunction getString(String delimiter) { return (types, collection) -> { diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/PrimitiveVariableContextVariableTypeConverter.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/PrimitiveVariableContextVariableTypeConverter.java index d41b3a333..310ceab09 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/PrimitiveVariableContextVariableTypeConverter.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/PrimitiveVariableContextVariableTypeConverter.java @@ -25,6 +25,7 @@ public class PrimitiveVariableContextVariableTypeConverter extends * @param clazz the class * @param fromPromptString the function to convert from a prompt string * @param fromObject the function to convert from an object to primitive + * @param toPromptString the function to convert to a prompt string */ public PrimitiveVariableContextVariableTypeConverter( Class clazz, diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/StringVariableContextVariableTypeConverter.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/StringVariableContextVariableTypeConverter.java index 4a431e609..3e303bff6 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/StringVariableContextVariableTypeConverter.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/StringVariableContextVariableTypeConverter.java @@ -29,6 +29,14 @@ public StringVariableContextVariableTypeConverter() { s -> s); } + /** + * Converts the specified object to a string. + * Has special handling for {@link ContextVariable} objects and + * for objects that look like an object reference + * @param s the object to convert + * @return the string representation of the object, or {@code null} + * if the object cannot be converted to a string or is an object reference. + */ @Nullable public static String convertToString(@Nullable Object s) { String converted = convert(s, String.class); diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/TextContentVariableContextVariableTypeConverter.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/TextContentVariableContextVariableTypeConverter.java index e43ed0f08..920ac4a18 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/TextContentVariableContextVariableTypeConverter.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/TextContentVariableContextVariableTypeConverter.java @@ -29,6 +29,12 @@ public TextContentVariableContextVariableTypeConverter() { }); } + /** + * Escapes the XML string value. + * @param value The value containing the content to escape. + * @return The escaped XML string value. + * @see ContextVariableTypeConverter#escapeXmlString(String) + */ @Nullable public static String escapeXmlStringValue(@Nullable TextContent value) { if (value == null) { diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/AutoFunctionChoiceBehavior.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/AutoFunctionChoiceBehavior.java new file mode 100644 index 000000000..b4993f44c --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/AutoFunctionChoiceBehavior.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.functionchoice; + +import com.microsoft.semantickernel.semanticfunctions.KernelFunction; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * A set of allowed kernel functions. All kernel functions are allowed if allKernelFunctionsAllowed is true. + * Otherwise, only the functions in allowedFunctions are allowed. + *

+ * If a function is allowed, it may be called. If it is not allowed, it will not be called. + */ +public class AutoFunctionChoiceBehavior extends FunctionChoiceBehavior { + private final boolean autoInvoke; + + /** + * Create a new instance of AutoFunctionChoiceBehavior. + * + * @param autoInvoke Whether auto-invocation is enabled. + * @param functions A set of functions to advertise to the model. + * @param options Options for the function choice behavior. + */ + public AutoFunctionChoiceBehavior(boolean autoInvoke, + @Nullable List> functions, + @Nullable FunctionChoiceBehaviorOptions options) { + super(functions, options); + this.autoInvoke = autoInvoke; + } + + /** + * Check whether the given function is allowed. + * + * @return Whether the function is allowed. + */ + public boolean isAutoInvoke() { + return autoInvoke; + } +} \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/FunctionChoiceBehavior.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/FunctionChoiceBehavior.java new file mode 100644 index 000000000..d74a77a79 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/FunctionChoiceBehavior.java @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.functionchoice; + +import com.microsoft.semantickernel.semanticfunctions.KernelFunction; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Defines the behavior of a tool call. Currently, the only tool available is function calling. + */ +public abstract class FunctionChoiceBehavior { + private final Set fullFunctionNames; + + protected final List> functions; + protected final FunctionChoiceBehaviorOptions options; + + protected FunctionChoiceBehavior(@Nullable List> functions, + @Nullable FunctionChoiceBehaviorOptions options) { + this.functions = functions != null ? Collections.unmodifiableList(functions) : null; + this.fullFunctionNames = new HashSet<>(); + + if (functions != null) { + functions.stream().filter(Objects::nonNull).forEach( + f -> this.fullFunctionNames + .add(formFullFunctionName(f.getPluginName(), f.getName()))); + } + + if (options != null) { + this.options = options; + } else { + this.options = FunctionChoiceBehaviorOptions.builder().build(); + } + } + + /** + * Gets the functions that are allowed. + * + * @return The functions that are allowed. + */ + public List> getFunctions() { + return Collections.unmodifiableList(functions); + } + + /** + * Gets the options for the function choice behavior. + * + * @return The options for the function choice behavior. + */ + public FunctionChoiceBehaviorOptions getOptions() { + return options; + } + + /** + * Gets an instance of the FunctionChoiceBehavior that provides all the Kernel's plugins functions to the AI model to call. + * + * @param autoInvoke Indicates whether the functions should be automatically invoked by AI connectors + * + * @return A new ToolCallBehavior instance with all kernel functions allowed. + */ + public static FunctionChoiceBehavior auto(boolean autoInvoke) { + return new AutoFunctionChoiceBehavior(autoInvoke, null, null); + } + + /** + * Gets an instance of the FunctionChoiceBehavior that provides either all the Kernel's plugins functions to the AI model to call or specific functions. + * + * @param autoInvoke Enable or disable auto-invocation. + * If auto-invocation is enabled, the model may request that the Semantic Kernel + * invoke the kernel functions and return the value to the model. + * @param functions Functions to provide to the model. If null, all the Kernel's plugins' functions are provided to the model. + * If empty, no functions are provided to the model, which is equivalent to disabling function calling. + * + * @return A new FunctionChoiceBehavior instance with all kernel functions allowed. + */ + public static FunctionChoiceBehavior auto(boolean autoInvoke, + @Nullable List> functions) { + return new AutoFunctionChoiceBehavior(autoInvoke, functions, null); + } + + /** + * Gets an instance of the FunctionChoiceBehavior that provides either all the Kernel's plugins functions to the AI model to call or specific functions. + * + * @param autoInvoke Enable or disable auto-invocation. + * If auto-invocation is enabled, the model may request that the Semantic Kernel + * invoke the kernel functions and return the value to the model. + * @param functions Functions to provide to the model. If null, all the Kernel's plugins' functions are provided to the model. + * If empty, no functions are provided to the model, which is equivalent to disabling function calling. + * @param options Options for the function choice behavior. + * + * @return A new FunctionChoiceBehavior instance with all kernel functions allowed. + */ + public static FunctionChoiceBehavior auto(boolean autoInvoke, + @Nullable List> functions, + @Nullable FunctionChoiceBehaviorOptions options) { + return new AutoFunctionChoiceBehavior(autoInvoke, functions, options); + } + + /** + * Gets an instance of the FunctionChoiceBehavior that provides either all the Kernel's plugins functions to the AI model to call or specific functions. + *

+ * This behavior forces the model to call the provided functions. + * SK connectors will invoke a requested function or multiple requested functions if the model requests multiple ones in one request, + * while handling the first request, and stop advertising the functions for the following requests to prevent the model from repeatedly calling the same function(s). + * + * @return A new FunctionChoiceBehavior instance with the required function. + */ + public static FunctionChoiceBehavior required(boolean autoInvoke, + @Nullable List> functions) { + return new RequiredFunctionChoiceBehavior(autoInvoke, functions, null); + } + + /** + * Gets an instance of the FunctionChoiceBehavior that provides either all the Kernel's plugins functions to the AI model to call or specific functions. + *

+ * This behavior forces the model to call the provided functions. + * SK connectors will invoke a requested function or multiple requested functions if the model requests multiple ones in one request, + * while handling the first request, and stop advertising the functions for the following requests to prevent the model from repeatedly calling the same function(s). + * + * @param functions Functions to provide to the model. If null, all the Kernel's plugins' functions are provided to the model. + * If empty, no functions are provided to the model, which is equivalent to disabling function calling. + * @return A new FunctionChoiceBehavior instance with the required function. + */ + public static FunctionChoiceBehavior required(boolean autoInvoke, + @Nullable List> functions, + @Nullable FunctionChoiceBehaviorOptions options) { + return new RequiredFunctionChoiceBehavior(autoInvoke, functions, options); + } + + /** + * Gets an instance of the FunctionChoiceBehavior that provides either all the Kernel's plugins functions to the AI model to call or specific functions. + *

+ * This behavior is useful if the user should first validate what functions the model will use. + */ + public static FunctionChoiceBehavior none() { + return new NoneFunctionChoiceBehavior(null, null); + } + + /** + * Gets an instance of the FunctionChoiceBehavior that provides either all the Kernel's plugins functions to the AI model to call or specific functions. + *

+ * This behavior is useful if the user should first validate what functions the model will use. + * + * @param functions Functions to provide to the model. If null, all the Kernel's plugins' functions are provided to the model. + * If empty, no functions are provided to the model, which is equivalent to disabling function calling. + */ + public static FunctionChoiceBehavior none(@Nullable List> functions, + @Nullable FunctionChoiceBehaviorOptions options) { + return new NoneFunctionChoiceBehavior(functions, options); + } + + /** + * The separator between the plugin name and the function name. + */ + public static final String FUNCTION_NAME_SEPARATOR = "-"; + + /** + * Form the full function name. + * + * @param pluginName The name of the plugin that the function is in. + * @param functionName The name of the function. + * @return The key for the function. + */ + public static String formFullFunctionName(@Nullable String pluginName, String functionName) { + if (pluginName == null) { + pluginName = ""; + } + return String.format("%s%s%s", pluginName, FUNCTION_NAME_SEPARATOR, functionName); + } + + /** + * Check whether the given function is allowed. + * + * @param function The function to check. + * @return Whether the function is allowed. + */ + public boolean isFunctionAllowed(KernelFunction function) { + return isFunctionAllowed(function.getPluginName(), function.getName()); + } + + /** + * Check whether the given function is allowed. + * + * @param pluginName The name of the plugin that the function is in. + * @param functionName The name of the function. + * @return Whether the function is allowed. + */ + public boolean isFunctionAllowed(@Nullable String pluginName, String functionName) { + // If no functions are provided, all functions are allowed. + if (functions == null || functions.isEmpty()) { + return true; + } + + String key = formFullFunctionName(pluginName, functionName); + return fullFunctionNames.contains(key); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/FunctionChoiceBehaviorOptions.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/FunctionChoiceBehaviorOptions.java new file mode 100644 index 000000000..ffb17c780 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/FunctionChoiceBehaviorOptions.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.functionchoice; + +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; + +public class FunctionChoiceBehaviorOptions { + private final boolean parallelCallsAllowed; + + private FunctionChoiceBehaviorOptions(boolean parallelCallsAllowed) { + this.parallelCallsAllowed = parallelCallsAllowed; + } + + /** + * Returns a new builder for {@link FunctionChoiceBehaviorOptions}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Indicates whether parallel calls to functions are allowed. + * + * @return True if parallel calls are allowed; otherwise, false. + */ + public boolean isParallelCallsAllowed() { + return parallelCallsAllowed; + } + + /** + * Builder for {@link FunctionChoiceBehaviorOptions}. + */ + public static class Builder implements SemanticKernelBuilder { + private boolean allowParallelCalls = false; + + /** + * Sets whether parallel calls to functions are allowed. + * + * @param allowParallelCalls True if parallel calls are allowed; otherwise, false. + * @return The builder instance. + */ + public Builder withParallelCallsAllowed(boolean allowParallelCalls) { + this.allowParallelCalls = allowParallelCalls; + return this; + } + + public FunctionChoiceBehaviorOptions build() { + return new FunctionChoiceBehaviorOptions(allowParallelCalls); + } + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/NoneFunctionChoiceBehavior.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/NoneFunctionChoiceBehavior.java new file mode 100644 index 000000000..1842ba5fc --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/NoneFunctionChoiceBehavior.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.functionchoice; + +import com.microsoft.semantickernel.semanticfunctions.KernelFunction; + +import javax.annotation.Nullable; +import java.util.List; + +public class NoneFunctionChoiceBehavior extends FunctionChoiceBehavior { + + /** + * Create a new instance of NoneFunctionChoiceBehavior. + */ + public NoneFunctionChoiceBehavior(@Nullable List> functions, + @Nullable FunctionChoiceBehaviorOptions options) { + super(functions, options); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/RequiredFunctionChoiceBehavior.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/RequiredFunctionChoiceBehavior.java new file mode 100644 index 000000000..8bfee535b --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/functionchoice/RequiredFunctionChoiceBehavior.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.functionchoice; + +import com.microsoft.semantickernel.semanticfunctions.KernelFunction; + +import javax.annotation.Nullable; +import java.util.List; + +public class RequiredFunctionChoiceBehavior extends AutoFunctionChoiceBehavior { + + /** + * Create a new instance of RequiredFunctionChoiceBehavior. + * + * @param autoInvoke Whether auto-invocation is enabled. + * @param functions A set of functions to advertise to the model. + * @param options Options for the function choice behavior. + */ + public RequiredFunctionChoiceBehavior(boolean autoInvoke, + @Nullable List> functions, + @Nullable FunctionChoiceBehaviorOptions options) { + super(autoInvoke, functions, options); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/FunctionInvokedEvent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/FunctionInvokedEvent.java index 8f2d7eed8..715e71185 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/FunctionInvokedEvent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/FunctionInvokedEvent.java @@ -2,8 +2,8 @@ package com.microsoft.semantickernel.hooks; import com.microsoft.semantickernel.orchestration.FunctionResult; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; @@ -16,7 +16,7 @@ public class FunctionInvokedEvent implements KernelHookEvent { private final KernelFunction function; @Nullable - private final KernelFunctionArguments arguments; + private final KernelArguments arguments; private final FunctionResult result; /** @@ -28,10 +28,10 @@ public class FunctionInvokedEvent implements KernelHookEvent { */ public FunctionInvokedEvent( KernelFunction function, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, FunctionResult result) { this.function = function; - this.arguments = KernelFunctionArguments.builder().withVariables(arguments).build(); + this.arguments = KernelArguments.builder().withVariables(arguments).build(); this.result = result; } @@ -51,7 +51,7 @@ public KernelFunction getFunction() { */ @SuppressFBWarnings("EI_EXPOSE_REP") @Nullable - public KernelFunctionArguments getArguments() { + public KernelArguments getArguments() { return arguments; } @@ -60,7 +60,6 @@ public KernelFunctionArguments getArguments() { * * @return the result */ - @SuppressFBWarnings("EI_EXPOSE_REP") public FunctionResult getResult() { return result; } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/FunctionInvokingEvent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/FunctionInvokingEvent.java index ab765cabe..ca360a760 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/FunctionInvokingEvent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/FunctionInvokingEvent.java @@ -2,7 +2,7 @@ package com.microsoft.semantickernel.hooks; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; @@ -16,7 +16,7 @@ public class FunctionInvokingEvent implements KernelHookEvent { private final KernelFunction function; - private final KernelFunctionArguments arguments; + private final KernelArguments arguments; /** * Creates a new instance of the FunctionInvokingEvent class. @@ -25,9 +25,9 @@ public class FunctionInvokingEvent implements KernelHookEvent { * @param arguments The arguments that are being passed to the function */ public FunctionInvokingEvent(KernelFunction function, - @Nullable KernelFunctionArguments arguments) { + @Nullable KernelArguments arguments) { this.function = function; - this.arguments = KernelFunctionArguments.builder().withVariables(arguments).build(); + this.arguments = KernelArguments.builder().withVariables(arguments).build(); } /** @@ -45,7 +45,7 @@ public KernelFunction getFunction() { * @return the arguments */ @SuppressFBWarnings("EI_EXPOSE_REP") - public KernelFunctionArguments getArguments() { + public KernelArguments getArguments() { return arguments; } } \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/KernelHook.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/KernelHook.java index f8f4b939e..586c93c5d 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/KernelHook.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/KernelHook.java @@ -98,11 +98,13 @@ interface PreChatCompletionHook extends KernelHook { static ChatCompletionsOptions cloneOptionsWithMessages( ChatCompletionsOptions options, List messages) { + ChatCompletionsOptions newOptions = new ChatCompletionsOptions(messages) .setPresencePenalty(options.getPresencePenalty()) .setFrequencyPenalty(options.getFrequencyPenalty()) .setLogitBias(options.getLogitBias()) .setMaxTokens(options.getMaxTokens()) + .setMaxCompletionTokens(options.getMaxCompletionTokens()) .setModel(options.getModel()) .setStop(options.getStop()) .setTemperature(options.getTemperature()) @@ -114,9 +116,11 @@ static ChatCompletionsOptions cloneOptionsWithMessages( .setFunctions(options.getFunctions()) .setN(options.getN()) .setResponseFormat(options.getResponseFormat()) - .setSeed(options.getSeed()) - .setStream(options.isStream()) - .setToolChoice(options.getToolChoice()); + .setSeed(options.getSeed()); + + if (options.getToolChoice() != null) { + newOptions.setToolChoice(options.getToolChoice()); + } if (options.getFunctionCall() != null) { newOptions = newOptions.setFunctionCall(options.getFunctionCall()); diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PreToolCallEvent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PreToolCallEvent.java index 9ef0d16c4..e5850228c 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PreToolCallEvent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PreToolCallEvent.java @@ -2,8 +2,8 @@ package com.microsoft.semantickernel.hooks; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; @@ -15,7 +15,7 @@ public class PreToolCallEvent implements KernelHookEvent { private final ContextVariableTypes contextVariableTypes; private final String functionName; @Nullable - private final KernelFunctionArguments arguments; + private final KernelArguments arguments; private final KernelFunction function; /** @@ -29,7 +29,7 @@ public class PreToolCallEvent implements KernelHookEvent { @SuppressFBWarnings("EI_EXPOSE_REP2") public PreToolCallEvent( String functionName, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, KernelFunction function, ContextVariableTypes contextVariableTypes) { this.functionName = functionName; @@ -38,13 +38,20 @@ public PreToolCallEvent( this.contextVariableTypes = contextVariableTypes; } + /** + * Gets the tool call arguments. + * @return The tool call arguments. + */ @SuppressFBWarnings("EI_EXPOSE_REP") @Nullable - public KernelFunctionArguments getArguments() { + public KernelArguments getArguments() { return arguments; } - @SuppressFBWarnings("EI_EXPOSE_REP2") + /** + * Get the tool call function. + * @return The tool call function. + */ public KernelFunction getFunction() { return function; } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PromptRenderedEvent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PromptRenderedEvent.java index f3f349725..fbd8cb801 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PromptRenderedEvent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PromptRenderedEvent.java @@ -2,7 +2,7 @@ package com.microsoft.semantickernel.hooks; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; @@ -12,7 +12,7 @@ public class PromptRenderedEvent implements KernelHookEvent { private final KernelFunction function; - private final KernelFunctionArguments arguments; + private final KernelArguments arguments; private final String prompt; /** @@ -24,10 +24,10 @@ public class PromptRenderedEvent implements KernelHookEvent { */ public PromptRenderedEvent( KernelFunction function, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, String prompt) { this.function = function; - this.arguments = KernelFunctionArguments.builder().withVariables(arguments).build(); + this.arguments = KernelArguments.builder().withVariables(arguments).build(); this.prompt = prompt; } @@ -46,7 +46,7 @@ public KernelFunction getFunction() { * @return the arguments */ @SuppressFBWarnings("EI_EXPOSE_REP") - public KernelFunctionArguments getArguments() { + public KernelArguments getArguments() { return arguments; } @@ -55,7 +55,6 @@ public KernelFunctionArguments getArguments() { * * @return the prompt */ - @SuppressFBWarnings("EI_EXPOSE_REP") public String getPrompt() { return prompt; } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PromptRenderingEvent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PromptRenderingEvent.java index 7bba17a38..fe9c54459 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PromptRenderingEvent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/hooks/PromptRenderingEvent.java @@ -2,7 +2,7 @@ package com.microsoft.semantickernel.hooks; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; @@ -12,7 +12,7 @@ public class PromptRenderingEvent implements KernelHookEvent { private final KernelFunction function; - private final KernelFunctionArguments arguments; + private final KernelArguments arguments; /** * Creates a new instance of the {@link PromptRenderingEvent} class. @@ -21,9 +21,9 @@ public class PromptRenderingEvent implements KernelHookEvent { * @param arguments the arguments */ public PromptRenderingEvent(KernelFunction function, - @Nullable KernelFunctionArguments arguments) { + @Nullable KernelArguments arguments) { this.function = function; - this.arguments = KernelFunctionArguments.builder().withVariables(arguments).build(); + this.arguments = KernelArguments.builder().withVariables(arguments).build(); } /** @@ -41,7 +41,7 @@ public KernelFunction getFunction() { * @return the arguments */ @SuppressFBWarnings("EI_EXPOSE_REP") - public KernelFunctionArguments getArguments() { + public KernelArguments getArguments() { return arguments; } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoader.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoader.java index 429338884..9bc9d661c 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoader.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoader.java @@ -2,6 +2,7 @@ package com.microsoft.semantickernel.implementation; import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; @@ -9,6 +10,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; +import java.text.MessageFormat; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -126,7 +128,10 @@ private static String readInputStream(String fileName, InputStream inputStream) return bf.lines().collect(Collectors.joining("\n")); } catch (IOException e) { // IGNORE - LOGGER.trace("Failed to load file: " + fileName, e); + LOGGER.trace( + MessageFormat.format(SemanticKernelResources.getString("failed.to.load.file.0"), + fileName), + e); } return null; } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/ServiceLoadUtil.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/ServiceLoadUtil.java index ede542462..fe4544dfe 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/ServiceLoadUtil.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/ServiceLoadUtil.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.ServiceLoader; import java.util.function.Supplier; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,11 +55,13 @@ public static Supplier findServiceLoader(Class clazz, String alternati | IllegalAccessException | NoSuchMethodException | RuntimeException e) { - LOGGER.error("Unable to load service " + clazz.getName() + " ", e); + LOGGER.error(String.format( + SemanticKernelResources.getString("unable.to.load.service.s"), clazz.getName()), + e); } if (impl == null) { - throw new RuntimeException("Service not found: " + clazz.getName()); + throw new RuntimeException(String.format("Service not found: %s", clazz.getName())); } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/chatcompletion/ChatXMLPromptParser.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/chatcompletion/ChatXMLPromptParser.java index 1c8f78c62..975f5e533 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/chatcompletion/ChatXMLPromptParser.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/chatcompletion/ChatXMLPromptParser.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.orchestration.ToolCallBehavior; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -17,6 +18,7 @@ import java.util.Locale; import java.util.Map; import javax.annotation.Nullable; +import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; @@ -31,6 +33,29 @@ public class ChatXMLPromptParser { private static final Logger LOGGER = LoggerFactory.getLogger(ChatXMLPromptParser.class); + private static XMLInputFactory createXMLInputFactory() { + XMLInputFactory factory = XMLInputFactory.newInstance(); + + trySetProperty(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + trySetProperty(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + trySetProperty(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); + + return factory; + } + + private static void trySetProperty(XMLInputFactory factory, String property, Object value) { + try { + factory.setProperty(property, value); + } catch (IllegalArgumentException e) { + // Property not supported by this XMLInputFactory implementation + LOGGER.trace("XMLInputFactory property '{}' not supported", property); + } + } + public static ChatPromptParseVisitor parse( String rawPrompt, ChatPromptParseVisitor chatPromptParseVisitor) { @@ -63,7 +88,7 @@ private static ChatPromptParseVisitor getChatRequestMessages(String promp // In this way, we can avoid parsing the whole prompt twice and easily extend the parsing logic. try (InputStream is = new ByteArrayInputStream(prompt.getBytes(StandardCharsets.UTF_8))) { - XMLInputFactory factory = XMLInputFactory.newInstance(); + XMLInputFactory factory = createXMLInputFactory(); XMLEventReader reader = factory.createXMLEventReader(is); while (reader.hasNext()) { XMLEvent event = reader.nextEvent(); @@ -108,7 +133,7 @@ private static ChatPromptParseVisitor getFunctionDefinitions(String promp // try (InputStream is = new ByteArrayInputStream(prompt.getBytes(StandardCharsets.UTF_8))) { - XMLInputFactory factory = XMLInputFactory.newFactory(); + XMLInputFactory factory = createXMLInputFactory(); XMLEventReader reader = factory.createXMLEventReader(is); FunctionDefinition functionDefinition = null; Map parameters = new HashMap<>(); @@ -203,7 +228,7 @@ private static ChatPromptParseVisitor getFunctionDefinitions(String promp } } } catch (IOException | XMLStreamException | IllegalArgumentException e) { - LOGGER.error("Error parsing prompt", e); + LOGGER.error(SemanticKernelResources.getString("error.parsing.prompt"), e); } return chatPromptParseVisitor; } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/ChatCompletionSpan.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/ChatCompletionSpan.java new file mode 100644 index 000000000..9fa465ed9 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/ChatCompletionSpan.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.implementation.telemetry; + +import com.azure.ai.openai.models.ChatCompletions; +import com.azure.ai.openai.models.CompletionsUsage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import java.util.function.Function; +import javax.annotation.Nullable; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +public class ChatCompletionSpan extends SemanticKernelTelemetrySpan { + + public ChatCompletionSpan( + Span span, + Function reactorContextModifier, + Scope spanScope, + Scope contextScope) { + super(span, reactorContextModifier, spanScope, contextScope); + } + + public static ChatCompletionSpan startChatCompletionSpan( + SemanticKernelTelemetry telemetry, + ContextView contextView, + @Nullable String modelName, + String modelProvider, + @Nullable Integer maxTokens, + @Nullable Double temperature, + @Nullable Double topP) { + return startCompletionSpan( + telemetry, + contextView, + "chat.completions", + modelName, + modelProvider, + maxTokens, + temperature, topP); + } + + public ChatCompletionSpan startTextCompletionSpan( + SemanticKernelTelemetry telemetry, + ContextView contextView, + @Nullable String modelName, + String modelProvider, + @Nullable Integer maxTokens, + @Nullable Double temperature, + @Nullable Double topP) { + return startCompletionSpan( + telemetry, + contextView, + "text.completions", + modelName, + modelProvider, + maxTokens, + temperature, topP); + } + + public static ChatCompletionSpan startCompletionSpan( + SemanticKernelTelemetry telemetry, + ContextView contextView, + String operationName, + @Nullable String modelName, + String modelProvider, + @Nullable Integer maxTokens, + @Nullable Double temperature, + @Nullable Double topP) { + if (modelName == null) { + modelName = "unknown"; + } + + SpanBuilder builder = telemetry.spanBuilder(operationName + " " + modelName) + .setSpanKind(SpanKind.CLIENT) + .setAttribute("gen_ai.request.model", modelName) + .setAttribute("gen_ai.operation.name", operationName) + .setAttribute("gen_ai.system", modelProvider); + + if (maxTokens != null) { + builder.setAttribute("gen_ai.request.max_tokens", maxTokens); + } + if (temperature != null) { + builder.setAttribute("gen_ai.request.temperature", temperature); + } + if (topP != null) { + builder.setAttribute("gen_ai.request.top_p", topP); + } + + Span span = builder.startSpan(); + + return build( + span, + contextView, + (contextModifier, spanScope, contextScope) -> new ChatCompletionSpan( + span, + contextModifier, + spanScope, + contextScope)); + } + + public void endSpanWithUsage(ChatCompletions chatCompletions) { + CompletionsUsage usage = chatCompletions.getUsage(); + getSpan().setStatus(StatusCode.OK); + getSpan() + .setAttribute("gen_ai.usage.output_tokens", usage.getCompletionTokens()); + getSpan().setAttribute("gen_ai.usage.input_tokens", usage.getPromptTokens()); + close(); + } + + public void endSpanWithError(Throwable throwable) { + getSpan().setStatus(StatusCode.ERROR, throwable.getMessage()); + close(); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/FunctionSpan.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/FunctionSpan.java new file mode 100644 index 000000000..72cc02609 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/FunctionSpan.java @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.implementation.telemetry; + +import com.microsoft.semantickernel.orchestration.FunctionResult; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import java.util.function.Function; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +public class FunctionSpan extends SemanticKernelTelemetrySpan { + + public FunctionSpan( + Span span, + Function reactorContextModifier, + Scope spanScope, + Scope contextScope) { + super(span, reactorContextModifier, spanScope, contextScope); + } + + public static FunctionSpan build( + SemanticKernelTelemetry telemetry, + ContextView contextView, + String pluginName, + String name, + KernelArguments arguments) { + + SpanBuilder builder = telemetry.spanBuilder( + String.format("function_invocation %s-%s", pluginName, name)) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("semantic_kernel.function.invocation.name", name) + .setAttribute("semantic_kernel.function.invocation.plugin_name", pluginName); + + Span span = builder.startSpan(); + + return build( + span, + contextView, + (contextModifier, spanScope, contextScope) -> new FunctionSpan( + span, + contextModifier, + spanScope, + contextScope)); + } + + public void onFunctionSuccess(FunctionResult result) { + try { + getSpan().setStatus(StatusCode.OK); + } finally { + close(); + } + } + + public void onFunctionError(Throwable error) { + try { + getSpan().setStatus(StatusCode.ERROR, error.getMessage()); + getSpan().recordException(error); + } finally { + close(); + } + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetry.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetry.java index 2ba4b8467..4121881d8 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetry.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetry.java @@ -1,79 +1,54 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.implementation.telemetry; -import com.azure.ai.openai.models.CompletionsUsage; +import com.microsoft.semantickernel.orchestration.InvocationContext; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.Tracer; import javax.annotation.Nullable; public class SemanticKernelTelemetry { public static final String OPEN_AI_PROVIDER = "openai"; - public static Span startChatCompletionSpan( - @Nullable String modelName, - String modelProvider, - @Nullable Integer maxTokens, - @Nullable Double temperature, - @Nullable Double topP) { - return startCompletionSpan("chat.completions", modelName, modelProvider, maxTokens, - temperature, topP); - } + private final Tracer tracer; - public static Span startTextCompletionSpan( - @Nullable String modelName, - String modelProvider, - @Nullable Integer maxTokens, - @Nullable Double temperature, - @Nullable Double topP) { - return startCompletionSpan("text.completions", modelName, modelProvider, maxTokens, - temperature, topP); - } + @Nullable + private final SpanContext spanContext; - private static Span startCompletionSpan( - String operationName, - @Nullable String modelName, - String modelProvider, - @Nullable Integer maxTokens, - @Nullable Double temperature, - @Nullable Double topP) { - OpenTelemetry otel = GlobalOpenTelemetry.get(); + public SemanticKernelTelemetry( + Tracer tracer, + @Nullable SpanContext spanContext) { - if (modelName == null) { - modelName = "unknown"; - } - SpanBuilder builder = otel - .getTracer("SemanticKernel") - .spanBuilder(operationName + " " + modelName) - .setAttribute("gen_ai.request.model", modelName) - .setAttribute("gen_ai.operation.name", operationName) - .setAttribute("gen_ai.system", modelProvider); + this.tracer = tracer; + this.spanContext = spanContext; + } - if (maxTokens != null) { - builder.setAttribute("gen_ai.request.max_tokens", maxTokens); - } - if (temperature != null) { - builder.setAttribute("gen_ai.request.temperature", temperature); - } - if (topP != null) { - builder.setAttribute("gen_ai.request.top_p", topP); - } + public SemanticKernelTelemetry() { + this( + GlobalOpenTelemetry.getTracer("SemanticKernel"), + null); + } - return builder.startSpan(); + public static SemanticKernelTelemetry getTelemetry( + @Nullable InvocationContext invocationContext) { + if (invocationContext != null) { + return invocationContext.getTelemetry(); + } + return new SemanticKernelTelemetry(); } - public static void endSpanWithUsage(Span span, CompletionsUsage usage) { - span.setStatus(StatusCode.OK); - span.setAttribute("gen_ai.response.completion_tokens", usage.getCompletionTokens()); - span.setAttribute("gen_ai.response.prompt_tokens", usage.getPromptTokens()); - span.end(); + private Tracer getTracer() { + return tracer; } - public static void endSpanWithError(Span span) { - span.setStatus(StatusCode.ERROR); - span.end(); + public SpanBuilder spanBuilder(String operationName) { + SpanBuilder sb = tracer.spanBuilder(operationName); + + if (spanContext != null) { + sb.addLink(spanContext); + } + return sb; } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetrySpan.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetrySpan.java new file mode 100644 index 000000000..2ef6413ce --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetrySpan.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.implementation.telemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +import java.io.Closeable; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.slf4j.Logger; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +public abstract class SemanticKernelTelemetrySpan implements Closeable { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger( + SemanticKernelTelemetrySpan.class); + + private static final long SPAN_TIMEOUT_MS = Long.parseLong((String) System.getProperties() + .getOrDefault("semantickernel.telemetry.span_timeout", "120000")); + + private final Span span; + private final Function reactorContextModifier; + private final Scope spanScope; + private final Scope contextScope; + private final AtomicBoolean closed = new AtomicBoolean(false); + + // Timeout to close the span if it was not closed within the specified time to avoid memory leaks + private final Disposable watchdog; + + // This is a finalizer guardian to ensure that the span is closed if it was not closed explicitly + @SuppressWarnings("unused") + private final Object finalizerGuardian = new Object() { + @Override + protected void finalize() { + if (closed.get() == false) { + LOGGER.warn("Span was not closed"); + close(); + } + } + }; + + public SemanticKernelTelemetrySpan(Span span, + Function reactorContextModifier, + Scope spanScope, Scope contextScope) { + this.span = span; + this.reactorContextModifier = reactorContextModifier; + this.spanScope = spanScope; + this.contextScope = contextScope; + + watchdog = Mono.just(1) + .delay(Duration.ofMillis(SPAN_TIMEOUT_MS)) + .subscribe(i -> { + if (closed.get() == false) { + LOGGER.warn("Span was not closed, timing out"); + close(); + } + }); + } + + public interface SpanConstructor { + + public T build( + Function contextModifier, + Scope spanScope, + Scope contextScope); + } + + // Does need to be closed but as we are doing this in a reactive app, cant enforce the try with resources + @SuppressWarnings("MustBeClosedChecker") + public static T build( + Span span, + ContextView contextView, + SpanConstructor builder) { + LOGGER.trace("Starting Span: {}", span); + + Context currentOtelContext = ContextPropagationOperator + .getOpenTelemetryContextFromContextView( + contextView, + Context.current()); + + Context otelContext = span.storeInContext(currentOtelContext); + Scope contextScope = otelContext.makeCurrent(); + Scope spanScope = span.makeCurrent(); + + Function reactorContextModifier = ctx -> { + return ContextPropagationOperator.storeOpenTelemetryContext(ctx, otelContext); + }; + + return builder.build(reactorContextModifier, spanScope, contextScope); + } + + public Function getReactorContextModifier() { + return reactorContextModifier; + } + + public void close() { + if (closed.compareAndSet(false, true)) { + LOGGER.trace("Closing span: {}", span); + if (span.isRecording()) { + try { + span.end(); + } catch (Exception e) { + LOGGER.error("Error closing span", e); + } + } + if (contextScope != null) { + try { + contextScope.close(); + } catch (Exception e) { + LOGGER.error("Error closing context scope", e); + } + } + if (spanScope != null) { + try { + spanScope.close(); + } catch (Exception e) { + LOGGER.error("Error closing span scope", e); + } + } + watchdog.dispose(); + } + } + + public Span getSpan() { + return span; + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/DefaultPromptTemplate.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/DefaultPromptTemplate.java index d0b70083a..15b80a662 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/DefaultPromptTemplate.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/DefaultPromptTemplate.java @@ -13,7 +13,7 @@ import com.microsoft.semantickernel.implementation.templateengine.tokenizer.blocks.VarBlock; import com.microsoft.semantickernel.orchestration.InvocationContext; import com.microsoft.semantickernel.semanticfunctions.InputVariable; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; import com.microsoft.semantickernel.templateengine.semantickernel.TemplateException; @@ -137,7 +137,7 @@ private static PromptTemplateConfig addMissingInputVariables( @Override public Mono renderAsync( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable InvocationContext context) { ContextVariableTypes types; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java index 50d7c469a..cd6fffaad 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java @@ -6,12 +6,14 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableType; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.InvocationContext; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionMetadata; import com.microsoft.semantickernel.templateengine.semantickernel.TemplateException; import com.microsoft.semantickernel.templateengine.semantickernel.TemplateException.ErrorCodes; +import java.text.MessageFormat; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -44,12 +46,14 @@ public CodeBlock(List tokens, String content) { public boolean isValid() { Optional invalid = tokens.stream().filter(token -> !token.isValid()).findFirst(); if (invalid.isPresent()) { - LOGGER.error("Invalid block" + invalid.get().getContent()); + LOGGER.error(MessageFormat.format(SemanticKernelResources.getString("invalid.block.0"), + invalid.get().getContent())); return false; } if (!this.tokens.isEmpty() && this.tokens.get(0).getType() == BlockTypes.NAMED_ARG) { - LOGGER.error("Unexpected named argument found. Expected function name first."); + LOGGER.error(SemanticKernelResources.getString( + "unexpected.named.argument.found.expected.function.name.first")); return false; } @@ -62,7 +66,9 @@ public boolean isValid() { private boolean isValidFunctionCall() { if (this.tokens.get(0).getType() != BlockTypes.FUNCTION_ID) { - LOGGER.error("Unexpected second token found: " + tokens.get(1).getContent()); + LOGGER.error(MessageFormat.format( + SemanticKernelResources.getString("unexpected.second.token.found.0"), + tokens.get(1).getContent())); return false; } @@ -70,15 +76,17 @@ private boolean isValidFunctionCall() { this.tokens.get(1).getType() != BlockTypes.VARIABLE && this.tokens.get(1).getType() != BlockTypes.NAMED_ARG) { LOGGER.error( - "The first arg of a function must be a quoted string, variable or named argument"); + SemanticKernelResources.getString( + "the.first.arg.of.a.function.must.be.a.quoted.string.variable.or.named.argument")); return false; } for (int i = 2; i < this.tokens.size(); i++) { if (this.tokens.get(i).getType() != BlockTypes.NAMED_ARG) { LOGGER.error( - "Functions only support named arguments after the first argument. Argument " + i - + " is not named."); + SemanticKernelResources.getString( + "functions.only.support.named.arguments.after.the.first.argument"), + i); return false; } } @@ -89,7 +97,7 @@ private boolean isValidFunctionCall() { @Override public Mono renderCodeAsync( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable InvocationContext context) { if (!this.isValid()) { throw new TemplateException(ErrorCodes.SYNTAX_ERROR); @@ -128,7 +136,7 @@ public Mono renderCodeAsync( private Mono> renderFunctionCallAsync( FunctionIdBlock fBlock, Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, InvocationContext context, ContextVariableType resultType) { @@ -137,7 +145,7 @@ private Mono> renderFunctionCallAsync( if (this.tokens.size() > 1) { //Cloning the original arguments to avoid side effects - arguments added to the original arguments collection as a result of rendering template variables. arguments = this.enrichFunctionArguments(kernel, fBlock, - KernelFunctionArguments.builder().withVariables(arguments).build(), + KernelArguments.builder().withVariables(arguments).build(), context); } @@ -160,10 +168,10 @@ private Mono> renderFunctionCallAsync( /// The prompt rendering arguments. /// The function arguments. /// Occurs when any argument other than the first is not a named argument. - private KernelFunctionArguments enrichFunctionArguments( + private KernelArguments enrichFunctionArguments( Kernel kernel, FunctionIdBlock fBlock, - KernelFunctionArguments arguments, + KernelArguments arguments, @Nullable InvocationContext context) { Block firstArg = this.tokens.get(1); diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeRendering.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeRendering.java index 7a411a095..673bcf68a 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeRendering.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeRendering.java @@ -3,7 +3,7 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.orchestration.InvocationContext; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import javax.annotation.Nullable; import reactor.core.publisher.Mono; @@ -24,6 +24,6 @@ public interface CodeRendering { */ Mono renderCodeAsync( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable InvocationContext context); } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/FunctionIdBlock.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/FunctionIdBlock.java index 58f8f5346..69e5a63be 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/FunctionIdBlock.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/FunctionIdBlock.java @@ -2,7 +2,7 @@ package com.microsoft.semantickernel.implementation.templateengine.tokenizer.blocks; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import javax.annotation.Nullable; /** @@ -49,7 +49,7 @@ private static boolean hasMoreThanOneDot(String value) { @Override @Nullable - public String render(ContextVariableTypes types, @Nullable KernelFunctionArguments variables) { + public String render(ContextVariableTypes types, @Nullable KernelArguments variables) { return this.getContent(); } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/NamedArgBlock.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/NamedArgBlock.java index 304031231..bed795020 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/NamedArgBlock.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/NamedArgBlock.java @@ -6,7 +6,8 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.exceptions.SKException; import com.microsoft.semantickernel.implementation.Verify; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.localization.SemanticKernelResources; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -109,18 +110,20 @@ private static String splitAndGetPart(String text, int x) { @Override public boolean isValid() { if (Verify.isNullOrEmpty(this.name)) { - LOGGER.error("A named argument must have a name"); + LOGGER.error(SemanticKernelResources.getString("a.named.argument.must.have.a.name")); return false; } if (this.valBlock != null && !this.valBlock.isValid()) { - LOGGER.error("There was an issue with the named argument value for '" + name); + LOGGER.error(SemanticKernelResources.getString( + "there.was.an.issue.with.the.named.argument.value.for"), name); return false; } else if (this.varBlock != null && !this.varBlock.isValid()) { - LOGGER.error("There was an issue with the named argument value for '" + name); + LOGGER.error(SemanticKernelResources.getString( + "there.was.an.issue.with.the.named.argument.value.for"), name); return false; } else if (this.valBlock == null && this.varBlock == null) { - LOGGER.error("A named argument must have a value"); + LOGGER.error(SemanticKernelResources.getString("a.named.argument.must.have.a.value")); return false; } @@ -129,7 +132,7 @@ public boolean isValid() { } @Override - public String render(ContextVariableTypes types, @Nullable KernelFunctionArguments variables) { + public String render(ContextVariableTypes types, @Nullable KernelArguments variables) { return getContent(); } @@ -152,7 +155,7 @@ public String getName() { } @SuppressWarnings("NullAway") - public String getValue(ContextVariableTypes types, KernelFunctionArguments arguments) { + public String getValue(ContextVariableTypes types, KernelArguments arguments) { boolean valueIsValidValBlock = this.valBlock != null && this.valBlock.isValid(); if (valueIsValidValBlock) { return this.valBlock.render(types, arguments); diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/TextBlock.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/TextBlock.java index 3b7d1c09c..38128538e 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/TextBlock.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/TextBlock.java @@ -2,7 +2,7 @@ package com.microsoft.semantickernel.implementation.templateengine.tokenizer.blocks; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import javax.annotation.Nullable; public final class TextBlock extends Block implements TextRendering { @@ -21,7 +21,7 @@ public boolean isValid() { } @Override - public String render(ContextVariableTypes types, @Nullable KernelFunctionArguments variables) { + public String render(ContextVariableTypes types, @Nullable KernelArguments variables) { return super.getContent(); } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/TextRendering.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/TextRendering.java index 751190742..184b4ca45 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/TextRendering.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/TextRendering.java @@ -2,7 +2,7 @@ package com.microsoft.semantickernel.implementation.templateengine.tokenizer.blocks; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import javax.annotation.Nullable; /** @@ -17,5 +17,5 @@ public interface TextRendering { * @return Rendered content */ @Nullable - String render(ContextVariableTypes types, @Nullable KernelFunctionArguments variables); + String render(ContextVariableTypes types, @Nullable KernelArguments variables); } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/ValBlock.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/ValBlock.java index bdd10f693..d81c84b9a 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/ValBlock.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/ValBlock.java @@ -2,7 +2,8 @@ package com.microsoft.semantickernel.implementation.templateengine.tokenizer.blocks; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.localization.SemanticKernelResources; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.util.annotation.Nullable; @@ -22,7 +23,8 @@ public ValBlock(String quotedValue) { super(quotedValue.trim(), BlockTypes.VALUE); if (this.getContent().length() < 2) { - LOGGER.error("A value must have single quotes or double quotes on both sides"); + LOGGER.error(SemanticKernelResources.getString( + "a.value.must.have.single.quotes.or.double.quotes.on.both.sides")); return; } @@ -39,7 +41,7 @@ public static boolean hasValPrefix(@Nullable String text) { @Override @Nullable - public String render(ContextVariableTypes types, @Nullable KernelFunctionArguments variables) { + public String render(ContextVariableTypes types, @Nullable KernelArguments variables) { return value; } @@ -47,15 +49,16 @@ public String render(ContextVariableTypes types, @Nullable KernelFunctionArgumen public boolean isValid() { // Content includes the quotes, so it must be at least 2 chars long if (this.getContent().length() < 2) { - LOGGER.error("A value must have single quotes or double quotes on both sides"); + LOGGER.error(SemanticKernelResources.getString( + "a.value.must.have.single.quotes.or.double.quotes.on.both.sides")); return false; } // Check if delimiting chars are consistent if (first != last) { LOGGER.error( - "A value must be defined using either single quotes or double quotes, not" - + " both"); + SemanticKernelResources.getString( + "a.value.must.be.defined.using.either.single.quotes.or.double.quotes.not.both")); return false; } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/VarBlock.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/VarBlock.java index 8514e8ab6..5ae70bb32 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/VarBlock.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/VarBlock.java @@ -3,7 +3,8 @@ import com.microsoft.semantickernel.contextvariables.ContextVariable; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.localization.SemanticKernelResources; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.templateengine.semantickernel.TemplateException; import javax.annotation.Nullable; import org.slf4j.Logger; @@ -18,14 +19,14 @@ public VarBlock(String content) { super(content, BlockTypes.VARIABLE); if (content.length() < 2) { - LOGGER.error("The variable name is empty"); + LOGGER.error(SemanticKernelResources.getString("the.variable.name.is.empty")); } this.name = content.substring(1); } @Override - public String render(ContextVariableTypes types, @Nullable KernelFunctionArguments variables) { + public String render(ContextVariableTypes types, @Nullable KernelArguments variables) { if (variables == null) { return ""; } @@ -40,7 +41,8 @@ public String render(ContextVariableTypes types, @Nullable KernelFunctionArgumen name); if (value == null) { - LOGGER.warn("Variable `{}{}` not found", Symbols.VarPrefix, name); + LOGGER.warn(SemanticKernelResources.getString("variable.not.found"), Symbols.VarPrefix, + name); } return value != null ? value.toPromptString(types) : ""; @@ -50,24 +52,27 @@ public String render(ContextVariableTypes types, @Nullable KernelFunctionArgumen public boolean isValid() { if (getContent() == null || getContent().isEmpty()) { LOGGER.error( - "A variable must start with the symbol {} and have a name", Symbols.VarPrefix); + SemanticKernelResources.getString( + "a.variable.must.start.with.the.symbol.and.have.a.name"), + Symbols.VarPrefix); return false; } if (getContent().charAt(0) != Symbols.VarPrefix) { - LOGGER.error("A variable must start with the symbol {}", Symbols.VarPrefix); + LOGGER.error(SemanticKernelResources.getString("a.variable.must.start.with.the.symbol"), + Symbols.VarPrefix); return false; } if (getContent().length() < 2) { - LOGGER.error("The variable name is empty"); + LOGGER.error(SemanticKernelResources.getString("the.variable.name.is.empty")); return false; } if (!name.matches("^[a-zA-Z0-9_]*$")) { LOGGER.error( - "The variable name '{}' contains invalid characters. " - + "Only alphanumeric chars and underscore are allowed.", + SemanticKernelResources.getString( + "the.variable.name.contains.invalid.characters.only.alphanumeric.chars.and.underscore.are.allowed"), name); return false; } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java index 4cd6ae2ac..8641f201c 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java @@ -6,13 +6,16 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableType; import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.contextvariables.converters.ContextVariableJacksonConverter; import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.functionchoice.FunctionChoiceBehavior; import com.microsoft.semantickernel.hooks.KernelHook; import com.microsoft.semantickernel.hooks.KernelHooks; import com.microsoft.semantickernel.hooks.KernelHooks.UnmodifiableKernelHooks; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; +import com.microsoft.semantickernel.localization.SemanticKernelResources; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.NoSuchElementException; import java.util.function.BiConsumer; import javax.annotation.Nullable; @@ -38,13 +41,18 @@ public class FunctionInvocation extends Mono> { protected final ContextVariableType resultType; protected final ContextVariableTypes contextVariableTypes = new ContextVariableTypes(); @Nullable - protected KernelFunctionArguments arguments; + protected KernelArguments arguments; @Nullable protected UnmodifiableKernelHooks hooks; @Nullable protected PromptExecutionSettings promptExecutionSettings; @Nullable protected ToolCallBehavior toolCallBehavior; + @Nullable + protected FunctionChoiceBehavior functionChoiceBehavior; + + @Nullable + protected SemanticKernelTelemetry telemetry; private boolean isSubscribed = false; @@ -54,7 +62,6 @@ public class FunctionInvocation extends Mono> { * @param kernel The kernel to invoke the function on. * @param function The function to invoke. */ - @SuppressFBWarnings("EI_EXPOSE_REP2") public FunctionInvocation( Kernel kernel, KernelFunction function) { @@ -71,7 +78,6 @@ public FunctionInvocation( * @param function The function to invoke. * @param resultType The type of the result of the function invocation. */ - @SuppressFBWarnings("EI_EXPOSE_REP2") public FunctionInvocation( Kernel kernel, KernelFunction function, @@ -90,26 +96,29 @@ private static void performSubscribe( CoreSubscriber> coreSubscriber, Kernel kernel, KernelFunction function, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext context) { if (variableType == null) { LOGGER.debug( - "No variable type explicitly specified by calling 'withResultType' for function invocation: " - + function.getPluginName() + "." + function.getName() + "." - + " This may cause a runtime error (probably a ClassCastException) if the result type is not compatible with the expected type."); + SemanticKernelResources.getString( + "no.variable.type.explicitly.specified.by.calling.withresulttype.for.function"), + function.getPluginName(), + function.getName()); } + InvocationContext contextClone = new InvocationContext(context); + function .invokeAsync( kernel, - KernelFunctionArguments + KernelArguments .builder() .withVariables(arguments) .build(), null, - new InvocationContext(context)) - .handle(convertToType(variableType)) + contextClone) + .handle(convertToType(variableType, contextClone.getContextVariableTypes())) .onErrorResume(e -> { if (e instanceof NoSuchElementException) { return Mono.empty(); @@ -121,13 +130,17 @@ private static void performSubscribe( } private static BiConsumer, SynchronousSink>> convertToType( - @Nullable ContextVariableType variableType) { + @Nullable ContextVariableType variableType, + @Nullable ContextVariableTypes contextVariableTypes) { return (result, sink) -> { // If a specific result type was requested, convert the result to that type. if (variableType != null) { try { + ContextVariableTypes types = new ContextVariableTypes(contextVariableTypes); + types.putConverter(variableType.getConverter()); + sink.next(new FunctionResult<>( - ContextVariable.convert(result.getResult(), variableType), + ContextVariable.convert(result.getResult(), variableType.getClazz(), types), result.getMetadata(), result.getUnconvertedResult())); } catch (Exception e) { @@ -162,9 +175,9 @@ private static UnmodifiableKernelHooks unmodifiableClone( * @return this {@code FunctionInvocation} for fluent chaining. */ public FunctionInvocation withArguments( - @Nullable KernelFunctionArguments arguments) { + @Nullable KernelArguments arguments) { logSubscribeWarning(); - this.arguments = KernelFunctionArguments.builder().withVariables(arguments).build(); + this.arguments = KernelArguments.builder().withVariables(arguments).build(); return this; } @@ -184,10 +197,28 @@ public FunctionInvocation withResultType(ContextVariableType resultTyp .withArguments(arguments) .addKernelHooks(hooks) .withPromptExecutionSettings(promptExecutionSettings) + .withFunctionChoiceBehavior(functionChoiceBehavior) .withToolCallBehavior(toolCallBehavior) .withTypes(contextVariableTypes); } + /** + * Supply the result type of function invocation. Also registers a type converter for the given + * type using {@code} ContextVariableJacksonConverter.create}. + * + * @param resultType The arguments to supply to the function invocation. + * @param The type of the result of the function invocation. + * @return A new {@code FunctionInvocation} for fluent chaining. + */ + public FunctionInvocation withResultTypeAutoConversion(Class resultType) { + try { + return withTypeConverter(ContextVariableJacksonConverter.create(resultType)) + .withResultType(contextVariableTypes.getVariableTypeForSuperClass(resultType)); + } catch (SKException e) { + return withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(resultType)); + } + } + /** * Supply the result type of function invocation. Uses the global context variable types. * @@ -258,10 +289,32 @@ public FunctionInvocation withPromptExecutionSettings( */ public FunctionInvocation withToolCallBehavior(@Nullable ToolCallBehavior toolCallBehavior) { logSubscribeWarning(); + if (toolCallBehavior != null && functionChoiceBehavior != null) { + throw new SKException( + "ToolCallBehavior cannot be set when FunctionChoiceBehavior is set."); + } this.toolCallBehavior = toolCallBehavior; return this; } + /** + * Supply function choice behavior to the function invocation. + * + * @param functionChoiceBehavior The function choice behavior to supply to the function + * invocation. + * @return this {@code FunctionInvocation} for fluent chaining. + */ + public FunctionInvocation withFunctionChoiceBehavior( + @Nullable FunctionChoiceBehavior functionChoiceBehavior) { + if (functionChoiceBehavior != null && toolCallBehavior != null) { + throw new SKException( + "FunctionChoiceBehavior cannot be set when ToolCallBehavior is set."); + } + logSubscribeWarning(); + this.functionChoiceBehavior = functionChoiceBehavior; + return this; + } + /** * Supply a type converter to the function invocation. * @@ -286,6 +339,17 @@ public FunctionInvocation withTypes(ContextVariableTypes contextVariableTypes return this; } + /** + * Supply a tracer to the function invocation. + * + * @param tracer The tracer to supply to the function invocation. + * @return this {@code FunctionInvocation} for fluent chaining. + */ + public FunctionInvocation withTelemetry(SemanticKernelTelemetry telemetry) { + this.telemetry = telemetry; + return this; + } + /** * Use an invocation context variable to supply the types, tool call behavior, prompt execution * settings, and kernel hooks to the function invocation. @@ -300,16 +364,19 @@ public FunctionInvocation withInvocationContext( } logSubscribeWarning(); withTypes(invocationContext.getContextVariableTypes()); + withFunctionChoiceBehavior(invocationContext.getFunctionChoiceBehavior()); withToolCallBehavior(invocationContext.getToolCallBehavior()); withPromptExecutionSettings(invocationContext.getPromptExecutionSettings()); addKernelHooks(invocationContext.getKernelHooks()); + withTelemetry(invocationContext.getTelemetry()); return this; } private void logSubscribeWarning() { if (isSubscribed) { LOGGER.warn( - "Attempting to modify function {}.{} after it has already been subscribed to. This is not necessarily an error but may be an unusual pattern and indicate a potential bug.", + SemanticKernelResources.getString( + "attempting.to.modify.function.after.it.has.already.been.subscribed"), function.getPluginName(), function.getName()); } } @@ -324,10 +391,15 @@ public void subscribe(CoreSubscriber> coreSubscriber) if (isSubscribed) { LOGGER.warn( - "Function {}.{} has already been subscribed to. This is not necessarily an error but may be an unusual pattern.", + SemanticKernelResources.getString( + "function.has.already.been.subscribed.to.this.is.not.necessarily.an.error.but.may.be.an.unusual.pattern"), function.getPluginName(), function.getName()); } + if (telemetry == null) { + telemetry = new SemanticKernelTelemetry(); + } + isSubscribed = true; performSubscribe( @@ -340,8 +412,10 @@ public void subscribe(CoreSubscriber> coreSubscriber) hooks, promptExecutionSettings, toolCallBehavior, + functionChoiceBehavior, contextVariableTypes, - InvocationReturnMode.NEW_MESSAGES_ONLY)); + InvocationReturnMode.NEW_MESSAGES_ONLY, + telemetry)); } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionResultMetadata.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionResultMetadata.java index f9e79c7f5..9bb55d11d 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionResultMetadata.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionResultMetadata.java @@ -12,6 +12,7 @@ * Metadata about the result of a function invocation. *

* This class is used to return metadata about the result of a function invocation. + * @param The result type of the function invocation. */ public class FunctionResultMetadata { @@ -50,6 +51,8 @@ public FunctionResultMetadata(CaseInsensitiveMap> metadata) { /** * Create a new instance of FunctionResultMetadata. + * @param id The id of the result of the function invocation. + * @return A new instance of FunctionResultMetadata. */ public static FunctionResultMetadata build(String id) { return build(id, null, null); @@ -61,6 +64,7 @@ public static FunctionResultMetadata build(String id) { * @param id The id of the result of the function invocation. * @param usage The usage of the result of the function invocation. * @param createdAt The time the result was created. + * @param The result type of the function invocation. * @return A new instance of FunctionResultMetadata. */ public static FunctionResultMetadata build( diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationContext.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationContext.java index 3f1ba7e01..6a8547c43 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationContext.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationContext.java @@ -4,8 +4,11 @@ import com.microsoft.semantickernel.builders.SemanticKernelBuilder; import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.functionchoice.FunctionChoiceBehavior; import com.microsoft.semantickernel.hooks.KernelHooks; import com.microsoft.semantickernel.hooks.KernelHooks.UnmodifiableKernelHooks; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; @@ -22,8 +25,11 @@ public class InvocationContext { private final PromptExecutionSettings promptExecutionSettings; @Nullable private final ToolCallBehavior toolCallBehavior; + @Nullable + private final FunctionChoiceBehavior functionChoiceBehavior; private final ContextVariableTypes contextVariableTypes; private final InvocationReturnMode invocationReturnMode; + private final SemanticKernelTelemetry telemetry; /** * Create a new instance of InvocationContext. @@ -37,17 +43,21 @@ protected InvocationContext( @Nullable KernelHooks hooks, @Nullable PromptExecutionSettings promptExecutionSettings, @Nullable ToolCallBehavior toolCallBehavior, + @Nullable FunctionChoiceBehavior functionChoiceBehavior, @Nullable ContextVariableTypes contextVariableTypes, - InvocationReturnMode invocationReturnMode) { + InvocationReturnMode invocationReturnMode, + SemanticKernelTelemetry telemetry) { this.hooks = unmodifiableClone(hooks); this.promptExecutionSettings = promptExecutionSettings; this.toolCallBehavior = toolCallBehavior; + this.functionChoiceBehavior = functionChoiceBehavior; this.invocationReturnMode = invocationReturnMode; if (contextVariableTypes == null) { this.contextVariableTypes = new ContextVariableTypes(); } else { this.contextVariableTypes = new ContextVariableTypes(contextVariableTypes); } + this.telemetry = telemetry; } /** @@ -57,8 +67,10 @@ protected InvocationContext() { this.hooks = null; this.promptExecutionSettings = null; this.toolCallBehavior = null; + this.functionChoiceBehavior = null; this.contextVariableTypes = new ContextVariableTypes(); this.invocationReturnMode = InvocationReturnMode.NEW_MESSAGES_ONLY; + this.telemetry = null; } /** @@ -71,14 +83,18 @@ protected InvocationContext(@Nullable InvocationContext context) { this.hooks = null; this.promptExecutionSettings = null; this.toolCallBehavior = null; + this.functionChoiceBehavior = null; this.contextVariableTypes = new ContextVariableTypes(); this.invocationReturnMode = InvocationReturnMode.NEW_MESSAGES_ONLY; + this.telemetry = null; } else { this.hooks = context.hooks; this.promptExecutionSettings = context.promptExecutionSettings; this.toolCallBehavior = context.toolCallBehavior; + this.functionChoiceBehavior = context.functionChoiceBehavior; this.contextVariableTypes = context.contextVariableTypes; this.invocationReturnMode = context.invocationReturnMode; + this.telemetry = context.telemetry; } } @@ -114,7 +130,8 @@ public static Builder copy(InvocationContext context) { .withKernelHooks(context.getKernelHooks()) .withContextVariableConverter(context.contextVariableTypes) .withPromptExecutionSettings(context.getPromptExecutionSettings()) - .withToolCallBehavior(context.getToolCallBehavior()); + .withToolCallBehavior(context.getToolCallBehavior()) + .withTelemetry(context.getTelemetry()); } /** @@ -148,6 +165,16 @@ public ToolCallBehavior getToolCallBehavior() { return toolCallBehavior; } + /** + * Get the behavior for function choice. + * + * @return The behavior for function choice. + */ + @Nullable + public FunctionChoiceBehavior getFunctionChoiceBehavior() { + return functionChoiceBehavior; + } + /** * Get the types of context variables. * @@ -166,6 +193,10 @@ public InvocationReturnMode returnMode() { return invocationReturnMode; } + public SemanticKernelTelemetry getTelemetry() { + return telemetry; + } + /** * Builder for {@link InvocationContext}. */ @@ -178,7 +209,11 @@ public static class Builder implements SemanticKernelBuilder private PromptExecutionSettings promptExecutionSettings; @Nullable private ToolCallBehavior toolCallBehavior; + @Nullable + private FunctionChoiceBehavior functionChoiceBehavior; private InvocationReturnMode invocationReturnMode = InvocationReturnMode.NEW_MESSAGES_ONLY; + @Nullable + private SemanticKernelTelemetry telemetry; /** * Add kernel hooks to the builder. @@ -212,10 +247,30 @@ public Builder withPromptExecutionSettings( */ public Builder withToolCallBehavior( @Nullable ToolCallBehavior toolCallBehavior) { + if (toolCallBehavior != null && functionChoiceBehavior != null) { + throw new SKException( + "ToolCallBehavior cannot be set when FunctionChoiceBehavior is set."); + } this.toolCallBehavior = toolCallBehavior; return this; } + /** + * Add function choice behavior to the builder. + * + * @param functionChoiceBehavior the behavior to add. + * @return this {@link Builder} + */ + public Builder withFunctionChoiceBehavior( + @Nullable FunctionChoiceBehavior functionChoiceBehavior) { + if (functionChoiceBehavior != null && toolCallBehavior != null) { + throw new SKException( + "FunctionChoiceBehavior cannot be set when ToolCallBehavior is set."); + } + this.functionChoiceBehavior = functionChoiceBehavior; + return this; + } + /** * Add a context variable type converter to the builder. * @@ -252,10 +307,25 @@ public Builder withReturnMode(InvocationReturnMode invocationReturnMode) { return this; } + /** + * Add a tracer to the builder. + * + * @param telemetry the tracer to add. + * @return this {@link Builder} + */ + public Builder withTelemetry(@Nullable SemanticKernelTelemetry telemetry) { + this.telemetry = telemetry; + return this; + } + @Override public InvocationContext build() { + if (telemetry == null) { + telemetry = new SemanticKernelTelemetry(); + } return new InvocationContext(hooks, promptExecutionSettings, toolCallBehavior, - contextVariableTypes, invocationReturnMode); + functionChoiceBehavior, + contextVariableTypes, invocationReturnMode, telemetry); } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationReturnMode.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationReturnMode.java index 1f03f22d5..48c279f20 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationReturnMode.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationReturnMode.java @@ -1,6 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.orchestration; +/** + * Represents the mode in which a function invocation should return its results. + */ public enum InvocationReturnMode { /** * Function invocations that build upon a history of previous invocations, such as Chat diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/PromptExecutionSettings.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/PromptExecutionSettings.java index 12dc0bbce..bfa2bb667 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/PromptExecutionSettings.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/PromptExecutionSettings.java @@ -3,11 +3,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.orchestration.responseformat.JsonObjectResponseFormat; +import com.microsoft.semantickernel.orchestration.responseformat.JsonSchemaResponseFormat; +import com.microsoft.semantickernel.orchestration.responseformat.ResponseFormat; +import com.microsoft.semantickernel.orchestration.responseformat.TextResponseFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; @@ -78,6 +82,7 @@ public class PromptExecutionSettings { private static final String PRESENCE_PENALTY = "presence_penalty"; private static final String FREQUENCY_PENALTY = "frequency_penalty"; private static final String MAX_TOKENS = "max_tokens"; + private static final String MAX_COMPLETION_TOKENS = "max_completion_tokens"; private static final String BEST_OF = "best_of"; private static final String USER = "user"; private static final String STOP_SEQUENCES = "stop_sequences"; @@ -85,19 +90,24 @@ public class PromptExecutionSettings { private static final String TOKEN_SELECTION_BIASES = "token_selection_biases"; private static final String RESPONSE_FORMAT = "response_format"; + private static final String MAX_COMPLETION_TOKENS_ENABLE = "MAX_COMPLETION_TOKENS_ENABLE"; + private static final String DEFAULT_MAX_COMPLETION_TOKENS_ENABLE = System.getenv( + MAX_COMPLETION_TOKENS_ENABLE); + private final String serviceId; private final String modelId; private final double temperature; private final double topP; private final double presencePenalty; private final double frequencyPenalty; - private final int maxTokens; + private final boolean maxCompletionTokensEnable; + private final Integer maxCompletionTokens; + private final Integer maxTokens; private final int bestOf; private final int resultsPerPrompt; private final String user; private final List stopSequences; private final Map tokenSelectionBiases; - @Nullable private final ResponseFormat responseFormat; /** @@ -115,7 +125,8 @@ public class PromptExecutionSettings { * @param user The user to associate with the prompt execution. * @param stopSequences The stop sequences to use for prompt execution. * @param tokenSelectionBiases The token selection biases to use for prompt execution. - * @param responseFormat The response format to use for prompt execution {@link ResponseFormat}. + * @param responseFormat The response format to use for prompt execution + * {@link ResponseFormat}, Defaults to TextResponseFormat. */ @JsonCreator public PromptExecutionSettings( @@ -131,14 +142,24 @@ public PromptExecutionSettings( @JsonProperty(USER) String user, @Nullable @JsonProperty(STOP_SEQUENCES) List stopSequences, @Nullable @JsonProperty(TOKEN_SELECTION_BIASES) Map tokenSelectionBiases, - @Nullable @JsonProperty(RESPONSE_FORMAT) String responseFormat) { + @Nullable @JsonProperty(RESPONSE_FORMAT) ResponseFormat responseFormat, + @JsonProperty(value = MAX_COMPLETION_TOKENS_ENABLE, defaultValue = "false") String maxCompletionTokensEnable) { this.serviceId = serviceId != null ? serviceId : DEFAULT_SERVICE_ID; this.modelId = modelId != null ? modelId : ""; this.temperature = clamp(temperature, 0d, 2d, DEFAULT_TEMPERATURE); this.topP = clamp(topP, 0d, 1d, DEFAULT_TOP_P); this.presencePenalty = clamp(presencePenalty, -2d, 2d, DEFAULT_PRESENCE_PENALTY); this.frequencyPenalty = clamp(frequencyPenalty, -2d, 2d, DEFAULT_FREQUENCY_PENALTY); - this.maxTokens = clamp(maxTokens, 1, Integer.MAX_VALUE, DEFAULT_MAX_TOKENS); + + this.maxCompletionTokensEnable = isMaxCompletionTokensEnable(maxCompletionTokensEnable); + + if (this.maxCompletionTokensEnable) { + this.maxCompletionTokens = clamp(maxTokens, 1, Integer.MAX_VALUE, DEFAULT_MAX_TOKENS); + this.maxTokens = null; + } else { + this.maxTokens = clamp(maxTokens, 1, Integer.MAX_VALUE, DEFAULT_MAX_TOKENS); + this.maxCompletionTokens = null; + } this.resultsPerPrompt = clamp(resultsPerPrompt, 1, Integer.MAX_VALUE, DEFAULT_RESULTS_PER_PROMPT); this.bestOf = clamp(bestOf, 1, Integer.MAX_VALUE, DEFAULT_BEST_OF); @@ -150,13 +171,29 @@ public PromptExecutionSettings( : Collections.emptyMap(); this.tokenSelectionBiases.replaceAll((k, v) -> clamp(v, -100, 100, 0)); - if (responseFormat != null && !responseFormat.isEmpty()) { - this.responseFormat = ResponseFormat.valueOf(responseFormat.toUpperCase(Locale.ROOT)); + if (responseFormat == null) { + this.responseFormat = new TextResponseFormat(); } else { - this.responseFormat = null; + this.responseFormat = responseFormat; } } + private boolean isMaxCompletionTokensEnable(String maxCompletionTokensEnable) { + final boolean maxCompletionTokensEnabled; + if (maxCompletionTokensEnable != null && !maxCompletionTokensEnable.isEmpty()) { + maxCompletionTokensEnabled = Boolean.parseBoolean(maxCompletionTokensEnable); + } else { + if (DEFAULT_MAX_COMPLETION_TOKENS_ENABLE != null + && DEFAULT_MAX_COMPLETION_TOKENS_ENABLE.isEmpty()) { + maxCompletionTokensEnabled = Boolean.parseBoolean( + DEFAULT_MAX_COMPLETION_TOKENS_ENABLE); + } else { + maxCompletionTokensEnabled = false; + } + } + return maxCompletionTokensEnabled; + } + /** * Create a new builder for PromptExecutionSettings. * @@ -253,7 +290,7 @@ public double getFrequencyPenalty() { * @return The maximum number of tokens to generate in the output. */ @JsonProperty(MAX_TOKENS) - public int getMaxTokens() { + public Integer getMaxTokens() { return maxTokens; } @@ -347,7 +384,10 @@ public boolean equals(Object obj) { if (Double.compare(frequencyPenalty, other.frequencyPenalty) != 0) { return false; } - if (maxTokens != other.maxTokens) { + if (!Objects.equals(maxTokens, other.maxTokens)) { + return false; + } + if (!Objects.equals(maxCompletionTokens, other.maxCompletionTokens)) { return false; } if (bestOf != other.bestOf) { @@ -374,11 +414,17 @@ public boolean equals(Object obj) { * * @return The response format to use for prompt execution. */ - @Nullable + @JsonProperty(RESPONSE_FORMAT) public ResponseFormat getResponseFormat() { return responseFormat; } + + @JsonProperty(MAX_COMPLETION_TOKENS) + public Integer getMaxCompletionTokens() { + return maxCompletionTokens; + } + /** * Builder for PromptExecutionSettings. */ @@ -476,6 +522,18 @@ public Builder withMaxTokens(int maxTokens) { return this; } + /** + * Enables the use of max_completion_tokens config parameter rather than the older + * max_completion + * + * @param enable Whether to enable + * @return This builder + */ + public Builder withMaxCompletionTokensEnable(boolean enable) { + settings.put(MAX_COMPLETION_TOKENS_ENABLE, Boolean.toString(enable)); + return this; + } + /** * Set the number of results to generate for each prompt. The value is clamped to the range * [1, Integer.MAX_VALUE], and the default is 1. @@ -550,7 +608,47 @@ public Builder withTokenSelectionBiases(Map tokenSelectionBias */ public Builder withResponseFormat(ResponseFormat responseFormat) { if (responseFormat != null) { - settings.put(RESPONSE_FORMAT, responseFormat.toString()); + settings.put(RESPONSE_FORMAT, responseFormat); + } + return this; + } + + /** + * Set the response format to use for prompt execution. + * + * @param responseFormat The response format to use for prompt execution. + * @return This builder. + */ + public Builder withResponseFormat(ResponseFormat.Type responseFormat) { + switch (responseFormat) { + case JSON_OBJECT: + settings.put(RESPONSE_FORMAT, new JsonObjectResponseFormat()); + break; + case TEXT: + settings.put(RESPONSE_FORMAT, new TextResponseFormat()); + break; + case JSON_SCHEMA: + throw new SKException( + "Cannot set JSON_SCHEMA response format without a schema, use withResponseFormat(ResponseFormat responseFormat)"); + } + + return this; + } + + /** + * Set the response format to use a json schema generated for the given class. The name of + * the response format will be the name of the class. + * + * @param responseFormat The response format type. + * @return This builder. + */ + public Builder withJsonSchemaResponseFormat(Class responseFormat) { + if (responseFormat != null) { + settings.put(RESPONSE_FORMAT, + JsonSchemaResponseFormat.builder() + .setResponseFormat(responseFormat) + .setName(responseFormat.getSimpleName()) + .build()); } return this; } @@ -576,7 +674,9 @@ public PromptExecutionSettings build() { (List) settings.getOrDefault(STOP_SEQUENCES, Collections.emptyList()), (Map) settings.getOrDefault(TOKEN_SELECTION_BIASES, Collections.emptyMap()), - (String) settings.getOrDefault(RESPONSE_FORMAT, null)); + (ResponseFormat) settings.getOrDefault(RESPONSE_FORMAT, new TextResponseFormat()), + (String) settings.getOrDefault(MAX_COMPLETION_TOKENS_ENABLE, + DEFAULT_MAX_COMPLETION_TOKENS_ENABLE)); } } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ResponseFormat.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ResponseFormat.java deleted file mode 100644 index 5442315e1..000000000 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ResponseFormat.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.orchestration; - -public enum ResponseFormat { - - /** - * Only valid for openai chat completion, with GPT-4 and gpt-3.5-turbo-1106+ models. - */ - JSON_OBJECT, TEXT; -} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ToolCallBehavior.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ToolCallBehavior.java index affa13ac2..6454a7fdb 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ToolCallBehavior.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ToolCallBehavior.java @@ -70,6 +70,9 @@ public static ToolCallBehavior allowOnlyKernelFunctions(boolean autoInvoke, } private static final int DEFAULT_MAXIMUM_AUTO_INVOKE_ATTEMPTS = 5; + /** + * The separator between the plugin name and the function name. + */ public static final String FUNCTION_NAME_SEPARATOR = "-"; private final int maximumAutoInvokeAttempts; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonObjectResponseFormat.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonObjectResponseFormat.java new file mode 100644 index 000000000..dde82691e --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonObjectResponseFormat.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.orchestration.responseformat; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * A response represented in a JSON format. + */ +public class JsonObjectResponseFormat extends ResponseFormat { + + /** + * Used by Jackson deserialization to create a new instance + * of the {@link JsonObjectResponseFormat} class. + */ + @JsonCreator + public JsonObjectResponseFormat() { + super(Type.JSON_OBJECT); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonResponseSchema.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonResponseSchema.java new file mode 100644 index 000000000..c952cfea7 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonResponseSchema.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.orchestration.responseformat; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The schema for a response in JSON format. + */ +public class JsonResponseSchema extends ResponseSchema { + + private final String name; + private final String schema; + private final boolean strict; + + /** + * Used by Jackson deserialization to create a new + * instance of the {@link JsonResponseSchema} class. + * + * @param name The name of the schema. + * @param schema The schema. + * @param strict Whether the schema is strict. + */ + @JsonCreator + public JsonResponseSchema( + @JsonProperty("name") String name, + @JsonProperty("schema") String schema, + @JsonProperty("strict") boolean strict) { + this.name = name; + this.schema = schema; + this.strict = strict; + } + + /** + * Gets the name of the schema. + * @return The name of the schema. + */ + public String getName() { + return name; + } + + /** + * Gets the schema. + * @return The schema. + */ + public String getSchema() { + return schema; + } + + /** + * Gets whether the schema is strict. + * @return Whether the schema is strict. + */ + public boolean isStrict() { + return strict; + } + +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonSchemaResponseFormat.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonSchemaResponseFormat.java new file mode 100644 index 000000000..ebc43b93b --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/JsonSchemaResponseFormat.java @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.orchestration.responseformat; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.exceptions.SKException; +import javax.annotation.Nullable; + +/** + * A response represented in a JSON schema format. + */ +public class JsonSchemaResponseFormat extends ResponseFormat { + + private final JsonResponseSchema jsonSchema; + + /** + * Used by Jackson deserialization to create a new instance + * of the {@link JsonSchemaResponseFormat} class. + * @param jsonSchema The JSON schema. + */ + @JsonCreator + public JsonSchemaResponseFormat( + @JsonProperty("json_schema") JsonResponseSchema jsonSchema) { + super(Type.JSON_SCHEMA); + this.jsonSchema = jsonSchema; + } + + /** + * Gets the JSON schema. + * @return The JSON schema. + */ + @JsonProperty("json_schema") + public JsonResponseSchema getJsonSchema() { + return jsonSchema; + } + + /** + * Creates a new instance of the {@link JsonSchemaResponseFormat} class. + * @return The new instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for the {@link JsonSchemaResponseFormat} class. + */ + public static class Builder { + + @Nullable + private JsonResponseSchema jsonResponseSchema = null; + @Nullable + private String jsonSchema = null; + @Nullable + private String name = null; + private boolean strict = true; + + /** + * Sets the response format. + * @param clazz The class. + * @param responseSchemaGenerator The response schema generator. + * @return The builder. + */ + public Builder setResponseFormat(Class clazz, + ResponseSchemaGenerator responseSchemaGenerator) { + name = clazz.getSimpleName(); + return setJsonSchema(responseSchemaGenerator.generateSchema(clazz)); + } + + /** + * Sets the response format. Uses Jackson to generate the schema + * from the {@code clazz} + * @param clazz The class. + * @return The builder. + */ + public Builder setResponseFormat(Class clazz) { + name = clazz.getSimpleName(); + setJsonSchema(ResponseSchemaGenerator.jacksonGenerator().generateSchema(clazz)); + return this; + } + + /** + * Sets the JSON response schema. + * @param jsonResponseSchema The JSON response schema. + * @return The builder. + */ + public Builder setJsonResponseSchema(JsonResponseSchema jsonResponseSchema) { + this.jsonResponseSchema = jsonResponseSchema; + return this; + } + + /** + * Sets the JSON schema. + * @param jsonSchema The JSON schema. + * @return The builder. + */ + public Builder setJsonSchema(String jsonSchema) { + this.jsonSchema = jsonSchema; + return this; + } + + /** + * Sets the name of the JSON schema. + * @param name The schema name. + * @return The builder. + */ + public Builder setName(String name) { + this.name = name; + return this; + } + + /** + * Sets whether the schema is strict. + * @param strict Whether the schema is strict. + * @return The builder. + */ + public Builder setStrict(boolean strict) { + this.strict = strict; + return this; + } + + /** + * Builds the {@link JsonSchemaResponseFormat} instance. + * @return The {@link JsonSchemaResponseFormat} instance. + */ + public JsonSchemaResponseFormat build() { + + if (jsonResponseSchema != null) { + return new JsonSchemaResponseFormat(jsonResponseSchema); + } + + if (jsonSchema == null) { + throw new SKException("Response format not set"); + } + + if (name == null) { + throw new SKException("Json format name not set"); + } + + return new JsonSchemaResponseFormat(new JsonResponseSchema(name, jsonSchema, strict)); + } + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseFormat.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseFormat.java new file mode 100644 index 000000000..9ebcbd660 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseFormat.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.orchestration.responseformat; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +/** + * Base class for response formats. + */ +@JsonTypeInfo(use = Id.NAME, include = As.EXISTING_PROPERTY, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = JsonSchemaResponseFormat.class, name = "json_schema", names = { + "json_schema", "JSON_SCHEMA" }), + @JsonSubTypes.Type(value = TextResponseFormat.class, name = "text", names = { "text", + "TEXT" }), + @JsonSubTypes.Type(value = JsonObjectResponseFormat.class, name = "json_object", names = { + "json_object", "JSON_OBJECT" }), + +}) +public abstract class ResponseFormat { + + /** + * The type of the response format. + */ + public static enum Type { + /** + * Only valid for openai chat completion, with GPT-4 and gpt-3.5-turbo-1106+ models. + */ + JSON_OBJECT, + /** + * Only valid for openai chat completion, with GPT-4 and gpt-3.5-turbo-1106+ models. + */ + JSON_SCHEMA, + /** + * The response is in text format. + */ + TEXT; + } + + private final Type type; + + /** + * Creates a new instance of the {@link ResponseFormat} class. + * + * @param type The type of the response format. + */ + public ResponseFormat(Type type) { + this.type = type; + } + + /** + * Gets the type of the response format. + * + * @return The type. + */ + @JsonProperty("type") + public Type getType() { + return type; + } + +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseSchema.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseSchema.java new file mode 100644 index 000000000..5d1403305 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseSchema.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.orchestration.responseformat; + +public abstract class ResponseSchema { + +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseSchemaGenerator.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseSchemaGenerator.java new file mode 100644 index 000000000..676f2872d --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/ResponseSchemaGenerator.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.orchestration.responseformat; + +import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.implementation.ServiceLoadUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Interface for generating json response schemas for a given class. + */ +public interface ResponseSchemaGenerator { + + Logger LOGGER = LoggerFactory.getLogger(ResponseSchemaGenerator.class); + + /** + * Generate a json schema for the given class. + * + * @param clazz The class to generate a schema for. + * @return The json schema. + */ + public String generateSchema(Class clazz); + + /** + * Load a response schema generator based on the Jackson library, requires that + * com.github.victools:jsonschema-generator has been added to the class path. + * + * @return The response schema generator. + */ + public static ResponseSchemaGenerator jacksonGenerator() { + try { + return loadGenerator( + "com.microsoft.semantickernel.aiservices.openai.chatcompletion.responseformat.JacksonResponseFormatGenerator"); + } catch (NoClassDefFoundError e) { + LOGGER.error( + "The Jackson response schema generator relies on the optional dependencies 'com.github.victools:jsonschema-generator', and 'com.github.victools:jsonschema-module-jackson'. To use this feature, please add this dependency to your project."); + throw new SKException( + "The Jackson response schema generator relies on the optional dependency 'com.github.victools:jsonschema-generator', and 'com.github.victools:jsonschema-module-jackson'. To use this feature, please add this dependency to your project."); + } + } + + /** + * Load a response schema generator based on the given class name. + * The class must implement the {@link ResponseSchemaGenerator} interface. + * + * @param className The class name of the generator. + * @return The response schema generator. + */ + public static ResponseSchemaGenerator loadGenerator(String className) { + return ServiceLoadUtil + .findServiceLoader(ResponseSchemaGenerator.class, + className) + .get(); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/TextResponseFormat.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/TextResponseFormat.java new file mode 100644 index 000000000..45214f31e --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/responseformat/TextResponseFormat.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.orchestration.responseformat; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * Represents a text response format. + */ +public class TextResponseFormat extends ResponseFormat { + + /** + * Used by Jackson to creates a new instance of the + * {@link TextResponseFormat} class. + */ + @JsonCreator + public TextResponseFormat() { + super(Type.TEXT); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/plugin/KernelPluginFactory.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/plugin/KernelPluginFactory.java index f6e8ad1e3..fa31819f2 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/plugin/KernelPluginFactory.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/plugin/KernelPluginFactory.java @@ -6,6 +6,7 @@ import com.microsoft.semantickernel.exceptions.SKException; import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader; import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader.ResourceLocation; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.semanticfunctions.InputVariable; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromMethod; @@ -24,12 +25,17 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; +import java.time.temporal.Temporal; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.reactivestreams.Publisher; @@ -49,7 +55,13 @@ public class KernelPluginFactory { private static final CaseInsensitiveMap> COMMON_CLASS_NAMES = new CaseInsensitiveMap<>(); private static final Map, Class> BOXED_FROM_PRIMITIVE = new HashMap<>(); + public static final String CLASS_FILTER_ENABLE_PROPERTY = "semantic-kernel.class-filter-enable"; + private static Boolean CLASS_FILTER_ENABLE; + static { + CLASS_FILTER_ENABLE = Boolean.parseBoolean( + System.getProperty(CLASS_FILTER_ENABLE_PROPERTY, "true")); + PRIMITIVE_CLASS_NAMES.put("void", void.class); PRIMITIVE_CLASS_NAMES.put("int", int.class); PRIMITIVE_CLASS_NAMES.put("double", double.class); @@ -66,6 +78,15 @@ public class KernelPluginFactory { COMMON_CLASS_NAMES.put("map", HashMap.class); COMMON_CLASS_NAMES.put("set", HashSet.class); + COMMON_CLASS_NAMES.put(Integer.class.getName(), int.class); + COMMON_CLASS_NAMES.put(String.class.getName(), String.class); + COMMON_CLASS_NAMES.put(List.class.getName(), ArrayList.class); + COMMON_CLASS_NAMES.put(Map.class.getName(), HashMap.class); + COMMON_CLASS_NAMES.put(Set.class.getName(), HashSet.class); + COMMON_CLASS_NAMES.put(Temporal.class.getName(), Temporal.class); + COMMON_CLASS_NAMES.put(java.time.OffsetDateTime.class.getName(), java.time.OffsetDateTime.class); + COMMON_CLASS_NAMES.put(java.time.ZonedDateTime.class.getName(), java.time.ZonedDateTime.class); + BOXED_FROM_PRIMITIVE.put(void.class, Void.class); BOXED_FROM_PRIMITIVE.put(int.class, Integer.class); BOXED_FROM_PRIMITIVE.put(double.class, Double.class); @@ -78,6 +99,10 @@ public class KernelPluginFactory { } + public static void setTypeFilterEnable(boolean enable) { + KernelPluginFactory.CLASS_FILTER_ENABLE = enable; + } + /** * Creates a plugin that wraps the specified target object. Methods decorated with * {@code {@literal @}DefineSKFunction} will be included in the plugin. @@ -143,8 +168,8 @@ public static KernelPlugin createFromObject(Class clazz, Object target, Strin if (plugin.getFunctions().isEmpty()) { LOGGER.warn( - "No functions found in class {}. This can be caused by DI frameworks that create proxies, or modules that are not making your methods visible. " - + "Try using: KernelPluginFactory.createFromObject(Class clazz, Object target, String pluginName).", + SemanticKernelResources.getString( + "no.functions.found.in.class.this.can.be.caused.by"), clazz.getName()); } return plugin; @@ -239,21 +264,16 @@ public static Class getTypeForName(String className) { return clazz; } + if (!checkClassName(className)) { + throw new SKException("Requested type is not allowed: " + className); + } + try { clazz = Thread.currentThread().getContextClassLoader().loadClass(className); } catch (ClassNotFoundException e) { // ignore } - if (clazz == null) { - try { - // Seems that in tests specifically we need to use the class loader of the class itself - clazz = KernelPluginFactory.class.getClassLoader().loadClass(className); - } catch (ClassNotFoundException e) { - // ignore - } - } - if (clazz == null) { throw new SKException("Requested type could not be found: " + className + ". This needs to be a fully qualified class name, e.g. 'java.lang.String'."); @@ -261,6 +281,13 @@ public static Class getTypeForName(String className) { return clazz; } + public static boolean checkClassName(String className) { + if (CLASS_FILTER_ENABLE == false) { + return true; + } + return ClassFilter.CLASS_CHECKER.test(className); + } + /** * Creates a plugin from the provided name and function collection. * @@ -360,7 +387,7 @@ public static KernelPlugin importPluginFromDirectory( plugins.put(dir.getName(), plugin); } catch (IOException e) { - LOGGER.error("Failed to read file", e); + LOGGER.error(SemanticKernelResources.getString("failed.to.read.file"), e); } } @@ -386,7 +413,8 @@ private static KernelFunction getKernelFunction( return getKernelFunction(functionName, promptTemplateFactory, config, template); } catch (Exception e) { - LOGGER.error("Failed to read file " + configPath.getAbsolutePath(), e); + LOGGER.error(SemanticKernelResources.getString("failed.to.read.file1"), + configPath.getAbsolutePath(), e); throw new SKException("Failed to read function " + configPath.getAbsolutePath(), e); } @@ -426,6 +454,8 @@ private static KernelFunction getKernelFunction( } /** + * Imports a plugin from a resource directory on the filesystem. + * * @param parentDirectory The parent directory containing the plugin directories. * @param pluginDirectoryName The name of the plugin directory. * @param functionName The name of the function to import. @@ -474,8 +504,10 @@ public static KernelPlugin importPluginFromResourcesDirectory( pluginDirectoryName, functionName, clazz); if (promptTemplateConfig == null) { - LOGGER.warn("Unable to load prompt template config for " + functionName + " in " - + pluginDirectoryName); + LOGGER.warn( + SemanticKernelResources.getString("unable.to.load.prompt.template.config.for.in"), + functionName, + pluginDirectoryName); return null; } KernelFunction function = getKernelFunction(functionName, promptTemplateFactory, @@ -503,7 +535,8 @@ private static String getTemplatePrompt( try { return getFileContents(promptFileName, clazz); } catch (IOException e) { - LOGGER.error("Failed to read file " + promptFileName, e); + LOGGER.error(SemanticKernelResources.getString("failed.to.read.file1"), promptFileName, + e); throw new SKException("No plugins found in directory " + promptFileName); } @@ -535,13 +568,118 @@ private static PromptTemplateConfig getPromptTemplateConfig( return PromptTemplateConfig.parseFromJson(config); } catch (Exception e) { if (e instanceof SKException) { - LOGGER.error("Failed to parse config file " + configFileName, e); + LOGGER.error(SemanticKernelResources.getString("failed.to.parse.config.file"), + configFileName, e); throw new SKException("Failed to parse config file " + configFileName, e); } else { - LOGGER.debug("No config for " + functionName + " in " + pluginName); + LOGGER.debug(SemanticKernelResources.getString("no.config.for.in"), functionName, + pluginName); } return null; } } + + // Filters allowed classes that can be used as types in plugins + public static class ClassFilter { + + // Selects which filter type to use, allow list or ban list + public static final String CLASS_BLOCK_TYPE_PROPERTY_NAME = "semantic-kernel.class-block-type"; + public static final String CLASS_BLOCK_LIST_PROPERTY_NAME = "semantic-kernel.class-block-list"; + public static final String CLASS_ALLOW_LIST_PROPERTY_NAME = "semantic-kernel.class-allow-list"; + + // allow nothing by default (other than java primitives and collections) + private static final List CLASS_ALLOW_LIST; + private static final List CLASS_ALLOW_LIST_DEFAULT = Collections.emptyList(); + + // block Java classes by default (other than java primitives and collections) + private static final List CLASS_BLOCK_LIST; + private static final List CLASS_BLOCK_LIST_DEFAULT = Arrays.asList( + "java\\..*", + "com\\.sun\\..*", + "javax\\..*", + "jdk\\..*", + "org\\.xml\\..*", + "org\\.w3c\\..*" + ); + + static Predicate CLASS_CHECKER; + + private enum BlockType { + BLOCK, + ALLOW + } + + static { + // Default to blocking type + String classFilterType = System.getProperty(CLASS_BLOCK_TYPE_PROPERTY_NAME, + BlockType.BLOCK.name()); + CLASS_BLOCK_LIST = getList(CLASS_BLOCK_LIST_PROPERTY_NAME, CLASS_BLOCK_LIST_DEFAULT); + CLASS_ALLOW_LIST = getList(CLASS_ALLOW_LIST_PROPERTY_NAME, CLASS_ALLOW_LIST_DEFAULT); + + BlockType type; + + try { + type = BlockType.valueOf(classFilterType.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + type = BlockType.BLOCK; + } + + switch (type) { + case ALLOW: + CLASS_CHECKER = ClassFilter::evaluateAllow; + break; + case BLOCK: + default: + CLASS_CHECKER = ClassFilter::evaluateBlock; + break; + } + } + + private static List getList(String propertyName, List defaultList) { + String blockList = System.getProperty(propertyName); + + if (blockList != null) { + return Arrays.asList(blockList.split(",")); + } else { + return defaultList; + } + } + + // Block classes/packages classes (other than common Java primitives and collections) + private static boolean evaluateBlock(String className) { + if (className == null || className.isEmpty()) { + return false; + } + + for (String ban : CLASS_BLOCK_LIST) { + if (className.matches(ban)) { + LOGGER.warn( + "Skipping class not allowed by class block list {}, if you wish to unblock this class update the property: {}. Filtering can also be controlled with {} and KernelPluginFactory.setTypeFilterEnable", + className, CLASS_BLOCK_LIST_PROPERTY_NAME, CLASS_FILTER_ENABLE_PROPERTY); + return false; + } + } + + return true; + } + + // Only allow explicitly allowed classes/packages (other than common Java primitives and collections) + private static boolean evaluateAllow(String className) { + if (className == null || className.isEmpty()) { + return false; + } + + for (String allow : CLASS_ALLOW_LIST) { + if (className.matches(allow)) { + return true; + } + } + + LOGGER.warn( + "Skipping class not allowed by class allow list {}, if you wish to allow this class update the property: {}. Filtering can also be controlled with {} and KernelPluginFactory.setTypeFilterEnable", + className, CLASS_ALLOW_LIST_DEFAULT, CLASS_FILTER_ENABLE_PROPERTY); + return false; + } + } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/HandlebarsPromptTemplateFactory.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/HandlebarsPromptTemplateFactory.java index 786df87ce..2a9b991ff 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/HandlebarsPromptTemplateFactory.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/HandlebarsPromptTemplateFactory.java @@ -18,6 +18,12 @@ public class HandlebarsPromptTemplateFactory implements PromptTemplateFactory { */ public static final String HANDLEBARS_TEMPLATE_FORMAT = "handlebars"; + /** + * Initializes a new instance of the {@link HandlebarsPromptTemplateFactory} class. + */ + public HandlebarsPromptTemplateFactory() { + } + @Override public PromptTemplate tryCreate(@NonNull PromptTemplateConfig templateConfig) { if (templateConfig.getTemplateFormat() != null && diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/InputVariable.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/InputVariable.java index a819b2ee2..800422632 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/InputVariable.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/InputVariable.java @@ -79,6 +79,7 @@ public InputVariable(String name) { * @param type the type of the input variable * @param description the description of the input variable * @param defaultValue the default value of the input variable + * @param enumValues the enum values of the input variable * @param required whether the input variable is required * @return a new instance of {@link InputVariable} */ diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelArguments.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelArguments.java new file mode 100644 index 000000000..bf9e65658 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelArguments.java @@ -0,0 +1,427 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.semanticfunctions; + +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.contextvariables.CaseInsensitiveMap; +import com.microsoft.semantickernel.contextvariables.ContextVariable; +import com.microsoft.semantickernel.contextvariables.ContextVariableType; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.exceptions.SKException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import reactor.util.annotation.NonNull; + +/** + * Arguments to a kernel function. + */ +public class KernelArguments implements Map> { + + /** + * Default key for the main input. + */ + public static final String MAIN_KEY = "input"; + + protected final CaseInsensitiveMap> variables; + protected final Map executionSettings; + + /** + * Create a new instance of KernelArguments. + * + * @param variables The variables to use for the function invocation. + */ + protected KernelArguments( + @Nullable Map> variables, + @Nullable Map executionSettings) { + if (variables == null) { + this.variables = new CaseInsensitiveMap<>(); + } else { + this.variables = new CaseInsensitiveMap<>(variables); + } + + if (executionSettings == null) { + this.executionSettings = new HashMap<>(); + } else { + this.executionSettings = new HashMap<>(executionSettings); + } + } + + /** + * Create a new instance of KernelArguments. + * + * @param content The content to use for the function invocation. + */ + protected KernelArguments(@NonNull ContextVariable content) { + this(); + this.variables.put(MAIN_KEY, content); + } + + /** + * Create a new instance of KernelArguments. + */ + protected KernelArguments() { + this.variables = new CaseInsensitiveMap<>(); + this.executionSettings = new HashMap<>(); + } + + /** + * Create a new instance of KernelArguments. + * + * @param arguments The arguments to copy. + */ + protected KernelArguments(@NonNull KernelArguments arguments) { + this.variables = new CaseInsensitiveMap<>(arguments.variables); + this.executionSettings = new HashMap<>(arguments.executionSettings); + } + + /** + * Get the prompt execution settings + * + * @return prompt execution settings + */ + @Nonnull + public Map getExecutionSettings() { + return Collections.unmodifiableMap(executionSettings); + } + + /** + * Get the input (entry in the MAIN_KEY slot) + * + * @return input + */ + @Nullable + public ContextVariable getInput() { + return get(MAIN_KEY); + } + + /** + * Create formatted string of the variables + * + * @return formatted string + */ + public String prettyPrint() { + return variables.entrySet().stream() + .reduce( + "", + (str, entry) -> str + + System.lineSeparator() + + entry.getKey() + + ": " + + entry.getValue().toPromptString(ContextVariableTypes.getGlobalTypes()), + (a, b) -> a + b); + } + + /** + * Return the variable with the given name + * + * @param key variable name + * @return content of the variable + */ + @Nullable + public ContextVariable get(String key) { + return variables.get(key); + } + + /** + * Return the variable with the given name + * + * @param key variable name + * @return content of the variable + */ + @Nullable + ContextVariable get(String key, Class clazz) { + ContextVariable value = variables.get(key); + if (value == null) { + return null; + } else if (clazz.isAssignableFrom(value.getType().getClazz())) { + return (ContextVariable) value; + } + + throw new SKException( + String.format( + "Variable %s is of type %s, but requested type is %s", + key, value.getType().getClazz(), clazz)); + } + + /** + * Return whether the variable with the given name is {@code null} or empty. + * + * @param key the key for the variable + * @return {@code true} if the variable is {@code null} or empty, {@code false} otherwise + */ + public boolean isNullOrEmpty(String key) { + return get(key) == null || get(key).isEmpty(); + } + + @Override + public int size() { + return variables.size(); + } + + @Override + public boolean isEmpty() { + return variables.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return variables.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return variables.containsValue(value); + } + + @Override + @Nullable + public ContextVariable get(Object key) { + return variables.get(key); + } + + @Override + public ContextVariable put(String key, ContextVariable value) { + return variables.put(key, value); + } + + @Override + public ContextVariable remove(Object key) { + return variables.remove(key); + } + + @Override + public void putAll(Map> m) { + variables.putAll(m); + } + + @Override + public void clear() { + variables.clear(); + } + + @Override + public Set keySet() { + return variables.keySet(); + } + + @Override + public Collection> values() { + return variables.values(); + } + + @Override + public Set>> entrySet() { + return variables.entrySet(); + } + + /** + * Create a copy of the current instance + * + * @return copy of the current instance + */ + public KernelArguments copy() { + return new KernelArguments(variables, executionSettings); + } + + /** + * Create a new instance of Builder. + * + * @return Builder + */ + public static Builder builder() { + return new Builder<>(KernelArguments::new); + } + + /** + * Builder for ContextVariables + */ + public static class Builder implements SemanticKernelBuilder { + + private final Function constructor; + private final Map> variables; + private final Map executionSettings; + + protected Builder(Function constructor) { + this.constructor = constructor; + this.variables = new HashMap<>(); + this.executionSettings = new HashMap<>(); + } + + /** + * Builds an instance with the given content in the default main key + * + * @param content Entry to place in the "input" slot + * @param Type of the value + * @return {$code this} Builder for fluent coding + */ + public Builder withInput(ContextVariable content) { + return withVariable(MAIN_KEY, content); + } + + /** + * Builds an instance with the given content in the default main key + * + * @param content Entry to place in the "input" slot + * @return {$code this} Builder for fluent coding + * @throws SKException if the content cannot be converted to a ContextVariable + */ + public Builder withInput(Object content) { + return withInput(ContextVariable.ofGlobalType(content)); + } + + /** + * Builds an instance with the given content in the default main key + * + * @param content Entry to place in the "input" slot + * @param typeConverter Type converter for the content + * @param Type of the value + * @return {$code this} Builder for fluent coding + * @throws SKException if the content cannot be converted to a ContextVariable + */ + public Builder withInput(T content, ContextVariableTypeConverter typeConverter) { + return withInput(new ContextVariable<>( + new ContextVariableType<>( + typeConverter, + typeConverter.getType()), + content)); + } + + /** + * Builds an instance with the given variables + * + * @param map Existing variables + * @return {$code this} Builder for fluent coding + */ + public Builder withVariables(@Nullable Map> map) { + if (map == null) { + return this; + } + variables.putAll(map); + return this; + } + + /** + * Set variable + * + * @param key variable name + * @param value variable value + * @param Type of the value + * @return {$code this} Builder for fluent coding + */ + public Builder withVariable(String key, ContextVariable value) { + variables.put(key, value); + return this; + } + + /** + * Set variable, uses the default type converters + * + * @param key variable name + * @param value variable value + * @return {$code this} Builder for fluent coding + * @throws SKException if the value cannot be converted to a ContextVariable + */ + public Builder withVariable(String key, Object value) { + if (value instanceof ContextVariable) { + return withVariable(key, (ContextVariable) value); + } + return withVariable(key, ContextVariable.ofGlobalType(value)); + } + + /** + * Set variable + * + * @param key variable name + * @param value variable value + * @param typeConverter Type converter for the value + * @param Type of the value + * @return {$code this} Builder for fluent coding + * @throws SKException if the value cannot be converted to a ContextVariable + */ + public Builder withVariable(String key, T value, + ContextVariableTypeConverter typeConverter) { + return withVariable(key, new ContextVariable<>( + new ContextVariableType<>( + typeConverter, + typeConverter.getType()), + value)); + } + + /** + * Set prompt execution settings + * + * @param executionSettings Execution settings + * @return {$code this} Builder for fluent coding + */ + public Builder withExecutionSettings(PromptExecutionSettings executionSettings) { + return withExecutionSettings(Collections.singletonList(executionSettings)); + } + + /** + * Set prompt execution settings + * + * @param executionSettings Execution settings + * @return {$code this} Builder for fluent coding + */ + public Builder withExecutionSettings( + Map executionSettings) { + if (executionSettings == null) { + return this; + } + + this.executionSettings.putAll(executionSettings); + return this; + } + + /** + * Set prompt execution settings + * + * @param executionSettings Execution settings + * @return {$code this} Builder for fluent coding + */ + public Builder withExecutionSettings(List executionSettings) { + if (executionSettings == null) { + return this; + } + + for (PromptExecutionSettings settings : executionSettings) { + String serviceId = settings.getServiceId(); + + if (this.executionSettings.containsKey(serviceId)) { + if (serviceId.equals(PromptExecutionSettings.DEFAULT_SERVICE_ID)) { + throw new SKException( + String.format( + "Multiple prompt execution settings with the default service id '%s' or no service id have been provided. Specify a single default prompt execution settings and provide a unique service id for all other instances.", + PromptExecutionSettings.DEFAULT_SERVICE_ID)); + } + + throw new SKException( + String.format( + "Multiple prompt execution settings with the service id '%s' have been provided. Specify a unique service id for all instances.", + serviceId)); + } + + this.executionSettings.put(serviceId, settings); + } + + return this; + } + + @Override + public U build() { + KernelArguments arguments = new KernelArguments(variables, executionSettings); + return constructor.apply(arguments); + } + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunction.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunction.java index 8dd31b6e4..742e84572 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunction.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunction.java @@ -99,6 +99,7 @@ public static FromPromptBuilder createFromPrompt( } /** + * Get the plugin name of the function. * @return The name of the plugin that this function is within */ @Nullable @@ -107,6 +108,7 @@ public String getPluginName() { } /** + * Get the name of the function. * @return The name of this function */ public String getName() { @@ -114,6 +116,7 @@ public String getName() { } /** + * Get the description of the function. * @return A description of the function */ @Nullable @@ -186,7 +189,7 @@ public KernelFunctionMetadata getMetadata() { */ public abstract Mono> invokeAsync( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext invocationContext); @@ -218,7 +221,7 @@ public abstract Mono> invokeAsync( */ public FunctionResult invoke( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext invocationContext) { return invokeAsync(kernel, arguments, variableType, invocationContext).block(); diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionArguments.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionArguments.java index f3b46c21a..76936cf23 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionArguments.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionArguments.java @@ -17,16 +17,17 @@ /** * Arguments to a kernel function. + * + * @deprecated Use {@link KernelArguments} instead. */ -public class KernelFunctionArguments implements Map> { +@Deprecated +public class KernelFunctionArguments extends KernelArguments { /** * Default key for the main input. */ public static final String MAIN_KEY = "input"; - private final CaseInsensitiveMap> variables; - /** * Create a new instance of KernelFunctionArguments. * @@ -34,11 +35,7 @@ public class KernelFunctionArguments implements Map> */ protected KernelFunctionArguments( @Nullable Map> variables) { - if (variables == null) { - this.variables = new CaseInsensitiveMap<>(); - } else { - this.variables = new CaseInsensitiveMap<>(variables); - } + super(variables, null); } /** @@ -47,15 +44,23 @@ protected KernelFunctionArguments( * @param content The content to use for the function invocation. */ protected KernelFunctionArguments(@NonNull ContextVariable content) { - this.variables = new CaseInsensitiveMap<>(); - this.variables.put(MAIN_KEY, content); + super(content); + } + + /** + * Create a new instance of KernelArguments. + * + * @param arguments The arguments to copy. + */ + protected KernelFunctionArguments(@NonNull KernelArguments arguments) { + super(arguments); } /** * Create a new instance of KernelFunctionArguments. */ protected KernelFunctionArguments() { - this.variables = new CaseInsensitiveMap<>(); + super(); } /** @@ -208,121 +213,18 @@ public KernelFunctionArguments copy() { /** * Builder for ContextVariables + * + * @deprecated Use {@link KernelArguments} builder instead. */ - public static class Builder implements SemanticKernelBuilder { - - private final Map> variables; + @Deprecated + public static class Builder extends KernelArguments.Builder { /** * Create a new instance of Builder. */ + @Deprecated public Builder() { - variables = new HashMap<>(); - } - - /** - * Builds an instance with the given content in the default main key - * - * @param content Entry to place in the "input" slot - * @param Type of the value - * @return {$code this} Builder for fluent coding - */ - public Builder withInput(ContextVariable content) { - return withVariable(MAIN_KEY, content); - } - - /** - * Builds an instance with the given content in the default main key - * - * @param content Entry to place in the "input" slot - * @return {$code this} Builder for fluent coding - * @throws SKException if the content cannot be converted to a ContextVariable - */ - public Builder withInput(Object content) { - return withInput(ContextVariable.ofGlobalType(content)); - } - - /** - * Builds an instance with the given content in the default main key - * - * @param content Entry to place in the "input" slot - * @param typeConverter Type converter for the content - * @param Type of the value - * @return {$code this} Builder for fluent coding - * @throws SKException if the content cannot be converted to a ContextVariable - */ - public Builder withInput(T content, ContextVariableTypeConverter typeConverter) { - return withInput(new ContextVariable<>( - new ContextVariableType<>( - typeConverter, - typeConverter.getType()), - content)); - } - - /** - * Builds an instance with the given variables - * - * @param map Existing variables - * @return {$code this} Builder for fluent coding - */ - public Builder withVariables(@Nullable Map> map) { - if (map == null) { - return this; - } - variables.putAll(map); - return this; - } - - /** - * Set variable - * - * @param key variable name - * @param value variable value - * @param Type of the value - * @return {$code this} Builder for fluent coding - */ - public Builder withVariable(String key, ContextVariable value) { - variables.put(key, value); - return this; - } - - /** - * Set variable, uses the default type converters - * - * @param key variable name - * @param value variable value - * @return {$code this} Builder for fluent coding - * @throws SKException if the value cannot be converted to a ContextVariable - */ - public Builder withVariable(String key, Object value) { - if (value instanceof ContextVariable) { - return withVariable(key, (ContextVariable) value); - } - return withVariable(key, ContextVariable.ofGlobalType(value)); - } - - /** - * Set variable - * - * @param key variable name - * @param value variable value - * @param typeConverter Type converter for the value - * @param Type of the value - * @return {$code this} Builder for fluent coding - * @throws SKException if the value cannot be converted to a ContextVariable - */ - public Builder withVariable(String key, T value, - ContextVariableTypeConverter typeConverter) { - return withVariable(key, new ContextVariable<>( - new ContextVariableType<>( - typeConverter, - typeConverter.getType()), - value)); - } - - @Override - public KernelFunctionArguments build() { - return new KernelFunctionArguments(variables); + super(KernelFunctionArguments::new); } } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java index 9a7b09dc4..6704255ff 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java @@ -14,6 +14,9 @@ import com.microsoft.semantickernel.hooks.FunctionInvokedEvent; import com.microsoft.semantickernel.hooks.FunctionInvokingEvent; import com.microsoft.semantickernel.hooks.KernelHooks; +import com.microsoft.semantickernel.implementation.telemetry.FunctionSpan; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.InvocationContext; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; @@ -22,6 +25,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -146,6 +150,14 @@ private static MethodDetails getMethodDetails( method.getReturnType())); } + /** + * Gets the function from the method. + * + * @param method the method to invoke + * @param instance the instance to invoke the method on + * @param the return type of the function + * @return the function representing the method + */ @SuppressWarnings("unchecked") public static ImplementationFunc getFunction(Method method, Object instance) { return (kernel, function, arguments, variableType, invocationContext) -> { @@ -164,7 +176,7 @@ public static ImplementationFunc getFunction(Method method, Object instan FunctionInvokingEvent updatedState = kernelHooks .executeHooks( new FunctionInvokingEvent(function, arguments)); - KernelFunctionArguments updatedArguments = updatedState != null + KernelArguments updatedArguments = updatedState != null ? updatedState.getArguments() : arguments; @@ -332,11 +344,11 @@ private static Mono invokeAsyncFunction( @Nullable private static Function getParameters( Method method, - @Nullable KernelFunctionArguments context, + @Nullable KernelArguments context, Kernel kernel, InvocationContext invocationContext) { return parameter -> { - if (KernelFunctionArguments.class.isAssignableFrom(parameter.getType())) { + if (KernelArguments.class.isAssignableFrom(parameter.getType())) { return context; } else if (Kernel.class.isAssignableFrom(parameter.getType())) { return kernel; @@ -349,7 +361,7 @@ private static Function getParameters( @Nullable private static Object getArgumentValue( Method method, - @Nullable KernelFunctionArguments context, + @Nullable KernelArguments context, Parameter parameter, Kernel kernel, InvocationContext invocationContext) { @@ -358,6 +370,8 @@ private static Object getArgumentValue( if (Kernel.class.isAssignableFrom(targetArgType)) { return kernel; + } else if (SemanticKernelTelemetry.class.isAssignableFrom(targetArgType)) { + return invocationContext.getTelemetry(); } String variableName = getGetVariableName(parameter); @@ -400,7 +414,7 @@ private static Object getArgumentValue( if (requestedType == String.class && !targetArgType.isAssignableFrom(requestedType)) { LOGGER.warn( - "Annotation on method: {} is requesting a String which is not assignable to method type {}, possibly as the type argument has not been provided on the annotation.", + SemanticKernelResources.getString("annotation.on.method.is.requesting.a.string"), method.getName(), targetArgType.getName()); } @@ -467,7 +481,7 @@ private static Object toObjectType( ContextVariableTypeConverter c = sourceType.getConverter(); Object converted = c.toObject(invocationContext.getContextVariableTypes(), sourceValue, - targetArgType); + targetArgType, false); if (converted != null) { return converted; } @@ -511,15 +525,15 @@ private static Object toObjectType( @Nullable private static ContextVariable getVariableFromContext( Method method, - @Nullable KernelFunctionArguments context, + @Nullable KernelArguments context, String variableName) { ContextVariable variable = context == null ? null : context.get(variableName); // If there is 1 argument use "input" or the only argument if (variable == null && method.getParameters().length == 1) { if (context != null) { - if (context.containsKey(KernelFunctionArguments.MAIN_KEY)) { - variable = context.get(KernelFunctionArguments.MAIN_KEY); + if (context.containsKey(KernelArguments.MAIN_KEY)) { + variable = context.get(KernelArguments.MAIN_KEY); } else if (context.size() == 1) { variable = context.values().iterator().next(); } @@ -632,21 +646,11 @@ private static String getGetVariableName(Parameter parameter) { private static String formErrorMessage(Method method, Parameter parameter) { Matcher matcher = Pattern.compile("arg(\\d)").matcher(parameter.getName()); matcher.find(); - return "For the function " - + method.getDeclaringClass().getName() - + "." - + method.getName() - + ", the unknown parameter" - + " name was detected as \"" - + parameter.getName() - + "\" this is argument" - + " number " - + matcher.group(1) - + " to the function, this indicates that the argument name for this function was" - + " removed during compilation and semantic-kernel is unable to determine the name" - + " of the parameter. To support this function the argument must be annotated with" - + " @SKFunctionParameters or @SKFunctionInputAttribute. Alternatively the function" - + " was invoked with a required context variable missing and no default value."; + return MessageFormat.format( + SemanticKernelResources.getString( + "for.the.function.0.1.the.unknown.parameter.name.was.detected"), + method.getDeclaringClass().getName(), method.getName(), parameter.getName(), + matcher.group(1)); } private static List getParameters(Method method) { @@ -668,7 +672,7 @@ private static InputVariable toKernelParameterMetadata(Parameter parameter) { boolean isRequired = true; Class type = parameter.getType(); - if (Kernel.class.isAssignableFrom(type) || KernelFunctionArguments.class.isAssignableFrom( + if (Kernel.class.isAssignableFrom(type) || KernelArguments.class.isAssignableFrom( type)) { return null; } @@ -691,6 +695,12 @@ private static InputVariable toKernelParameterMetadata(Parameter parameter) { isRequired); } + /** + * Gets the constants from an enum type. + * + * @param type the type to get the enum constants from + * @return a list of the enum constants or {@code null} if the type is not an enum + */ public static @Nullable List getEnumOptions(Class type) { List enumValues = null; if (type.isEnum()) { @@ -719,14 +729,31 @@ public static Builder builder() { @Override public Mono> invokeAsync( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext invocationContext) { - return function.invokeAsync(kernel, this, arguments, variableType, invocationContext); + + return Mono.deferContextual(contextView -> { + FunctionSpan span = FunctionSpan.build( + SemanticKernelTelemetry.getTelemetry(invocationContext), + contextView, + this.getPluginName(), + this.getName(), + arguments); + + return function + .invokeAsync(kernel, this, arguments, variableType, invocationContext) + .contextWrite(span.getReactorContextModifier()) + .doOnSuccess(span::onFunctionSuccess) + .doOnError(span::onFunctionError) + .doOnTerminate(span::close); + }); } /** * Concrete implementation of the abstract method in KernelFunction. + * + * @param the return type of the function */ public interface ImplementationFunc { @@ -743,7 +770,7 @@ public interface ImplementationFunc { Mono> invokeAsync( Kernel kernel, KernelFunction function, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext invocationContext); @@ -760,7 +787,7 @@ Mono> invokeAsync( default FunctionResult invoke( Kernel kernel, KernelFunction function, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext invocationContext) { return invokeAsync(kernel, function, arguments, variableType, @@ -770,6 +797,8 @@ default FunctionResult invoke( /** * A builder for {@link KernelFunction}. + * + * @param the return type of the function */ public static class Builder { diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java index e3be61e8d..642b5addc 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java @@ -10,6 +10,9 @@ import com.microsoft.semantickernel.hooks.KernelHooks; import com.microsoft.semantickernel.hooks.PromptRenderedEvent; import com.microsoft.semantickernel.hooks.PromptRenderingEvent; +import com.microsoft.semantickernel.implementation.telemetry.FunctionSpan; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.InvocationContext; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; @@ -20,6 +23,7 @@ import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -94,7 +98,7 @@ public static Builder builder(Class returnType) { private Flux> invokeInternalAsync( Kernel kernel, - @Nullable KernelFunctionArguments argumentsIn, + @Nullable KernelArguments argumentsIn, @Nullable ContextVariableType contextVariableType, @Nullable InvocationContext invocationContext) { @@ -109,7 +113,7 @@ private Flux> invokeInternalAsync( PromptRenderingEvent preRenderingHookResult = kernelHooks .executeHooks(new PromptRenderingEvent(this, argumentsIn)); - KernelFunctionArguments arguments = preRenderingHookResult.getArguments(); + KernelArguments arguments = preRenderingHookResult.getArguments(); // TODO: put in method, add catch for classcastexception, fallback to noopconverter ContextVariableType variableType = contextVariableType != null @@ -123,13 +127,17 @@ private Flux> invokeInternalAsync( PromptRenderedEvent promptHookResult = kernelHooks .executeHooks(new PromptRenderedEvent(this, arguments, prompt)); prompt = promptHookResult.getPrompt(); - KernelFunctionArguments args = promptHookResult.getArguments(); + KernelArguments args = promptHookResult.getArguments(); - LOGGER.info("RENDERED PROMPT: \n{}", prompt); + LOGGER.info(SemanticKernelResources.getString("rendered.prompt"), prompt); - FunctionInvokingEvent updateArguments = kernelHooks + FunctionInvokingEvent invokingEvent = kernelHooks .executeHooks(new FunctionInvokingEvent(this, args)); - args = updateArguments.getArguments(); + + args = KernelArguments.builder() + .withVariables(invokingEvent.getArguments()) + .withExecutionSettings(this.getExecutionSettings()) + .build(); AIServiceSelection aiServiceSelection = kernel .getServiceSelector() @@ -245,9 +253,8 @@ private Flux> invokeInternalAsync( .doOnError( ex -> { LOGGER.warn( - "Something went wrong while rendering the semantic" - + " function or while executing the text" - + " completion. Function: {}.{}. Error: {}", + SemanticKernelResources.getString( + "something.went.wrong.while.rendering.the.semantic.function.or.while.executing.the.text.completion.function.error"), getPluginName(), getName(), ex.getMessage()); @@ -259,16 +266,10 @@ private Flux> invokeInternalAsync( if (ex instanceof HttpResponseException && ((HttpResponseException) ex).getResponse().getStatusCode() == 400 && ex.getMessage() != null - && ex.getMessage().contains("parameters are not available" + " on")) { + && ex.getMessage().contains("parameters are not available on")) { LOGGER.warn( - "This error indicates that you have attempted" - + " to use a chat completion model in a" - + " text completion service. Try using a" - + " chat completion service instead when" - + " building your kernel, for instance when" - + " building your service use" - + " SKBuilders.chatCompletion() rather than" - + " SKBuilders.textCompletionService()."); + SemanticKernelResources.getString( + "this.error.indicates.that.you.have.attempted.to.use.a.chat.completion.model")); } }); } @@ -276,11 +277,25 @@ private Flux> invokeInternalAsync( @Override public Mono> invokeAsync( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext invocationContext) { - return invokeInternalAsync(kernel, arguments, variableType, invocationContext) - .takeLast(1).single(); + return Mono.deferContextual(contextView -> { + FunctionSpan span = FunctionSpan.build( + SemanticKernelTelemetry.getTelemetry(invocationContext), + contextView, + this.getPluginName(), + this.getName(), + arguments); + + return invokeInternalAsync(kernel, arguments, variableType, invocationContext) + .contextWrite(span.getReactorContextModifier()) + .takeLast(1) + .single() + .doOnSuccess(span::onFunctionSuccess) + .doOnError(span::onFunctionError) + .doOnTerminate(span::close); + }); } /** @@ -439,6 +454,7 @@ public KernelFunction build() { name, template, templateFormat, + Collections.emptySet(), description, inputVariables, outputVariable, @@ -454,6 +470,5 @@ public KernelFunction build() { return new KernelFunctionFromPrompt<>(temp, config, executionSettings); } - } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelPromptTemplateFactory.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelPromptTemplateFactory.java index b10c6fb93..45dd8428d 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelPromptTemplateFactory.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelPromptTemplateFactory.java @@ -16,6 +16,12 @@ */ public class KernelPromptTemplateFactory implements PromptTemplateFactory { + /** + * Initializes a new instance of the {@code KernelPromptTemplateFactory} class. + */ + public KernelPromptTemplateFactory() { + } + @Override public PromptTemplate tryCreate(@Nonnull PromptTemplateConfig templateConfig) { if (templateConfig.getTemplate() == null) { diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/OutputVariable.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/OutputVariable.java index 8871c03f5..833aef171 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/OutputVariable.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/OutputVariable.java @@ -9,6 +9,7 @@ /** * Metadata for an output variable of a kernel function. + * @param The type of the output variable. */ public class OutputVariable { diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplate.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplate.java index 04f0c6432..58a5c6a81 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplate.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplate.java @@ -24,7 +24,7 @@ public interface PromptTemplate { */ Mono renderAsync( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable InvocationContext context); } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateConfig.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateConfig.java index 02adb2896..cbc0478ba 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateConfig.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateConfig.java @@ -11,9 +11,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import javax.annotation.Nullable; /** @@ -43,6 +45,7 @@ public class PromptTemplateConfig { @Nullable private final String template; private final String templateFormat; + private final Set promptTemplateOptions; @Nullable private final String description; private final List inputVariables; @@ -61,6 +64,7 @@ protected PromptTemplateConfig(String template) { DEFAULT_CONFIG_NAME, template, SEMANTIC_KERNEL_TEMPLATE_FORMAT, + Collections.emptySet(), "", Collections.emptyList(), new OutputVariable(String.class.getName(), "out"), @@ -70,25 +74,30 @@ protected PromptTemplateConfig(String template) { /** * Constructor for a prompt template config * - * @param schema Schema version - * @param name Name of the template - * @param template Template string - * @param templateFormat Template format - * @param description Description of the template - * @param inputVariables Input variables - * @param outputVariable Output variable - * @param executionSettings Execution settings + * @param schema Schema version + * @param name Name of the template + * @param template Template string + * @param templateFormat Template format + * @param promptTemplateOptions Prompt template options + * @param description Description of the template + * @param inputVariables Input variables + * @param outputVariable Output variable + * @param executionSettings Execution settings */ @JsonCreator public PromptTemplateConfig( - @JsonProperty("schema") int schema, + @Nullable @JsonProperty("schema") Integer schema, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("template") String template, @Nullable @JsonProperty(value = "template_format", defaultValue = SEMANTIC_KERNEL_TEMPLATE_FORMAT) String templateFormat, + @Nullable @JsonProperty(value = "prompt_template_options") Set promptTemplateOptions, @Nullable @JsonProperty("description") String description, @Nullable @JsonProperty("input_variables") List inputVariables, @Nullable @JsonProperty("output_variable") OutputVariable outputVariable, @Nullable @JsonProperty("execution_settings") Map executionSettings) { + if (schema == null) { + schema = CURRENT_SCHEMA; + } this.schema = schema; this.name = name; this.template = template; @@ -96,6 +105,10 @@ public PromptTemplateConfig( templateFormat = SEMANTIC_KERNEL_TEMPLATE_FORMAT; } this.templateFormat = templateFormat; + if (promptTemplateOptions == null) { + promptTemplateOptions = new HashSet<>(); + } + this.promptTemplateOptions = promptTemplateOptions; this.description = description; if (inputVariables == null) { this.inputVariables = new ArrayList<>(); @@ -127,6 +140,7 @@ protected PromptTemplateConfig( @Nullable String name, @Nullable String template, @Nullable String templateFormat, + @Nullable Set promptTemplateOptions, @Nullable String description, @Nullable List inputVariables, @Nullable OutputVariable outputVariable, @@ -136,6 +150,7 @@ protected PromptTemplateConfig( name, template, templateFormat, + promptTemplateOptions, description, inputVariables, outputVariable, @@ -152,6 +167,7 @@ public PromptTemplateConfig(PromptTemplateConfig promptTemplate) { promptTemplate.name, promptTemplate.template, promptTemplate.templateFormat, + promptTemplate.promptTemplateOptions, promptTemplate.description, promptTemplate.inputVariables, promptTemplate.outputVariable, @@ -300,6 +316,15 @@ public int getSchema() { return schema; } + /** + * Get the prompt template options of the prompt template config. + * + * @return The prompt template options of the prompt template config. + */ + public Set getPromptTemplateOptions() { + return Collections.unmodifiableSet(promptTemplateOptions); + } + /** * Create a builder for a prompt template config which is a clone of the current object. * @@ -358,6 +383,7 @@ public static class Builder { @Nullable private String template; private String templateFormat = SEMANTIC_KERNEL_TEMPLATE_FORMAT; + private final Set promptTemplateOptions = new HashSet<>(); @Nullable private String description = null; private List inputVariables = new ArrayList<>(); @@ -382,7 +408,7 @@ private Builder(PromptTemplateConfig promptTemplateConfig) { * Set the name of the prompt template config. * * @param name The name of the prompt template config. - * @return {@code this} prompt template config. + * @return {@code this} builder */ public Builder withName(String name) { this.name = name; @@ -393,7 +419,7 @@ public Builder withName(String name) { * Add an input variable to the prompt template config. * * @param inputVariable The input variable to add. - * @return {@code this} prompt template config. + * @return {@code this} builder */ public Builder addInputVariable(InputVariable inputVariable) { inputVariables.add(inputVariable); @@ -404,7 +430,7 @@ public Builder addInputVariable(InputVariable inputVariable) { * Set the template of the prompt template config. * * @param template The template of the prompt template config. - * @return {@code this} prompt template config. + * @return {@code this} builder */ public Builder withTemplate(String template) { this.template = template; @@ -415,7 +441,7 @@ public Builder withTemplate(String template) { * Set the description of the prompt template config. * * @param description The description of the prompt template config. - * @return {@code this} prompt template config. + * @return {@code this} builder */ public Builder withDescription(String description) { this.description = description; @@ -426,18 +452,28 @@ public Builder withDescription(String description) { * Set the template format of the prompt template config. * * @param templateFormat The template format of the prompt template config. - * @return {@code this} prompt template config. + * @return {@code this} builder */ public Builder withTemplateFormat(String templateFormat) { this.templateFormat = templateFormat; return this; } + /** + * Set the prompt template options. + * @param option The prompt template option to add. + * @return {@code this} builder. + */ + public Builder addPromptTemplateOption(PromptTemplateOption option) { + promptTemplateOptions.add(option); + return this; + } + /** * Set the inputVariables of the prompt template config. * * @param inputVariables The input variables of the prompt template config. - * @return {@code this} prompt template config. + * @return {@code this} builder */ public Builder withInputVariables(List inputVariables) { this.inputVariables = new ArrayList<>(inputVariables); @@ -448,7 +484,7 @@ public Builder withInputVariables(List inputVariables) { * Set the output variable of the prompt template config. * * @param outputVariable The output variable of the prompt template config. - * @return {@code this} prompt template config. + * @return {@code this} builder */ public Builder withOutputVariable(OutputVariable outputVariable) { this.outputVariable = outputVariable; @@ -459,7 +495,7 @@ public Builder withOutputVariable(OutputVariable outputVariable) { * Set the prompt execution settings of the prompt template config. * * @param executionSettings The prompt execution settings of the prompt template config. - * @return {@code this} prompt template config. + * @return {@code this} builder */ public Builder withExecutionSettings( Map executionSettings) { @@ -477,6 +513,7 @@ public PromptTemplateConfig build() { name, template, templateFormat, + promptTemplateOptions, description, inputVariables, outputVariable, diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateOption.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateOption.java new file mode 100644 index 000000000..56f5a2e26 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateOption.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.semanticfunctions; + +/** + * Options to customize the behavior of a prompt. + */ +public enum PromptTemplateOption { + /** + * Allow methods on objects provided as arguments to an invocation, to be invoked when rendering + * a template and its return value used. Typically, this would be used to call a getter on an + * object i.e. {@code {{#each users}} {{userName}} {{/each}} } on a handlebars template will + * call the method {@code getUserName()} on each object in {@code users}. + *

+ * WARNING: If this option is used, ensure that your template is trusted, and that objects added + * as arguments to an invocation, do not contain methods that are unsafe to be invoked when + * rendering a template. + */ + ALLOW_CONTEXT_VARIABLE_METHOD_CALLS_UNSAFE +} \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/AIServiceSelector.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/AIServiceSelector.java index 3255d79e0..ac95f7d35 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/AIServiceSelector.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/AIServiceSelector.java @@ -2,7 +2,7 @@ package com.microsoft.semantickernel.services; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import javax.annotation.Nullable; /** @@ -15,12 +15,12 @@ public interface AIServiceSelector { /** * Resolves an {@link AIService} and associated and * {@link com.microsoft.semantickernel.orchestration.PromptExecutionSettings} based on the - * associated {@link KernelFunction} and {@link KernelFunctionArguments}. + * associated {@link KernelFunction} and {@link KernelArguments}. * * @param serviceType The type of service to select. This must be the same type with which the * service was registered in the {@link AIServiceSelection} * @param function The KernelFunction to use to select the service, or {@code null}. - * @param arguments The KernelFunctionArguments to use to select the service, or + * @param arguments The KernelArguments to use to select the service, or * {@code null}. * @param The type of service to select. * @return An {@code AIServiceSelection} containing the selected service and associated @@ -30,5 +30,26 @@ public interface AIServiceSelector { AIServiceSelection trySelectAIService( Class serviceType, @Nullable KernelFunction function, - @Nullable KernelFunctionArguments arguments); + @Nullable KernelArguments arguments); + + /** + * Resolves an {@link AIService} and associated and + * {@link com.microsoft.semantickernel.orchestration.PromptExecutionSettings} based on the + * associated {@link KernelFunction} and {@link KernelArguments}. + * + * @param serviceType The type of service to select. This must be the same type with which the + * service was registered in the {@link AIServiceSelection} + * @param arguments The KernelArguments to use to select the service, or + * {@code null}. + * @param The type of service to select. + * @return An {@code AIServiceSelection} containing the selected service and associated + * PromptExecutionSettings. + */ + @Nullable + default AIServiceSelection trySelectAIService( + Class serviceType, + @Nullable KernelArguments arguments) { + throw new UnsupportedOperationException( + "This method is not implemented."); + } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/BaseAIServiceSelector.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/BaseAIServiceSelector.java index 2eaa9404c..b022bf358 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/BaseAIServiceSelector.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/BaseAIServiceSelector.java @@ -2,16 +2,16 @@ package com.microsoft.semantickernel.services; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import java.util.Map; import javax.annotation.Nullable; /** * Base class for {@link AIServiceSelector} implementations which provides a {@code Map} based * collection from which an {@link AIService} can be selected. The - * {@link #trySelectAIService(Class, KernelFunction, KernelFunctionArguments)} method has been + * {@link #trySelectAIService(Class, KernelFunction, KernelArguments)} method has been * implemented. Child classes must implement the method - * {@link #trySelectAIService(Class, KernelFunction, KernelFunctionArguments, Map)}. + * {@link #trySelectAIService(Class, KernelFunction, KernelArguments, Map)}. */ public abstract class BaseAIServiceSelector implements AIServiceSelector { @@ -31,10 +31,18 @@ protected BaseAIServiceSelector(AIServiceCollection services) { public AIServiceSelection trySelectAIService( Class serviceType, @Nullable KernelFunction function, - @Nullable KernelFunctionArguments arguments) { + @Nullable KernelArguments arguments) { return trySelectAIService(serviceType, function, arguments, services); } + @Override + @Nullable + public AIServiceSelection trySelectAIService( + Class serviceType, + @Nullable KernelArguments arguments) { + return trySelectAIService(serviceType, arguments, services); + } + /** * Resolves an {@link AIService} from the {@code services} argument using the specified * {@code function} and {@code arguments} for selection. @@ -47,11 +55,32 @@ public AIServiceSelection trySelectAIService( * @param services The services to select from. * @param The type of service to select. * @return The selected service, or {@code null} if no service could be selected. + * */ @Nullable protected abstract AIServiceSelection trySelectAIService( Class serviceType, @Nullable KernelFunction function, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, Map, AIService> services); + + /** + * Resolves an {@link AIService} from the {@code services} argument using the specified + * {@code function} and {@code arguments} for selection. + * + * @param serviceType The type of service to select. This must be the same type with which the + * service was registered in the {@link AIServiceSelection} + * @param arguments The KernelArguments to use to select the service, or + * {@code null}. + * @param services The services to select from. + * @param The type of service to select. + * @return The selected service, or {@code null} if no service could be selected. + */ + @Nullable + protected AIServiceSelection trySelectAIService( + Class serviceType, + @Nullable KernelArguments arguments, + Map, AIService> services) { + return trySelectAIService(serviceType, null, arguments, services); + } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/KernelContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/KernelContent.java index e55f2cdce..6c8c80bc7 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/KernelContent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/KernelContent.java @@ -9,63 +9,23 @@ * * @param The type of the content. */ -public abstract class KernelContent { +public interface KernelContent { - /* + /** * The inner content representation. Use this to bypass the current * abstraction. The usage of this property is considered "unsafe". * Use it only if strictly necessary. + * @return The inner content. */ @Nullable - private final T innerContent; - - /** - * The model ID used to generate the content. - */ - @Nullable - private final String modelId; + T getInnerContent(); /** * The metadata associated with the content. + * @return The metadata. */ @Nullable - private final FunctionResultMetadata metadata; - - /** - * Initializes a new instance of the {@link KernelContent} class. - * - * @param innerContent The inner content representation. - * @param modelId The model identifier used to generate the content. - * @param metadata The metadata associated with the content. - */ - public KernelContent( - @Nullable T innerContent, - @Nullable String modelId, - @Nullable FunctionResultMetadata metadata) { - this.innerContent = innerContent; - this.modelId = modelId; - this.metadata = metadata; - } - - /** - * Gets the inner content representation. - * - * @return The inner content representation. - */ - @Nullable - public T getInnerContent() { - return innerContent; - } - - /** - * Gets the metadata associated with the content. - * - * @return The metadata associated with the content. - */ - @Nullable - public FunctionResultMetadata getMetadata() { - return metadata; - } + FunctionResultMetadata getMetadata(); /** * Gets the content returned by the AI service. @@ -73,5 +33,5 @@ public FunctionResultMetadata getMetadata() { * @return The content. */ @Nullable - public abstract String getContent(); + String getContent(); } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/KernelContentImpl.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/KernelContentImpl.java new file mode 100644 index 000000000..e83e909df --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/KernelContentImpl.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.services; + +import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; +import javax.annotation.Nullable; + +/** + * Base class which represents the content returned by an AI service. + * + * @param The type of the content. + */ +public abstract class KernelContentImpl implements KernelContent { + + /* + * The inner content representation. Use this to bypass the current + * abstraction. The usage of this property is considered "unsafe". + * Use it only if strictly necessary. + */ + @Nullable + private final T innerContent; + + /** + * The model ID used to generate the content. + */ + @Nullable + private final String modelId; + + /** + * The metadata associated with the content. + */ + @Nullable + private final FunctionResultMetadata metadata; + + /** + * Initializes a new instance of the {@link KernelContentImpl} class. + * + * @param innerContent The inner content representation. + * @param modelId The model identifier used to generate the content. + * @param metadata The metadata associated with the content. + */ + public KernelContentImpl( + @Nullable T innerContent, + @Nullable String modelId, + @Nullable FunctionResultMetadata metadata) { + this.innerContent = innerContent; + this.modelId = modelId; + this.metadata = metadata; + } + + /** + * Initializes a new instance of the {@link KernelContentImpl} class. + */ + public KernelContentImpl() { + this(null, null, null); + } + + /** + * Gets the inner content representation. + * + * @return The inner content representation. + */ + @Nullable + @Override + public T getInnerContent() { + return innerContent; + } + + /** + * Gets the metadata associated with the content. + * + * @return The metadata associated with the content. + */ + @Nullable + @Override + public FunctionResultMetadata getMetadata() { + return metadata; + } + +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/OrderedAIServiceSelector.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/OrderedAIServiceSelector.java index 411ba9877..30828233d 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/OrderedAIServiceSelector.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/OrderedAIServiceSelector.java @@ -2,9 +2,10 @@ package com.microsoft.semantickernel.services; import com.microsoft.semantickernel.implementation.Verify; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; import java.util.List; @@ -56,26 +57,25 @@ private static AIServiceSelection castServiceSelection( } } - @Nullable - private static Map settingsFromFunctionSettings( - @Nullable KernelFunction function) { - if (function != null) { - return function.getExecutionSettings(); - } - return null; - } - @Nullable @Override public AIServiceSelection trySelectAIService( Class serviceType, @Nullable KernelFunction function, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, Map, AIService> services) { - // Allow the execution settings from the kernel arguments to take precedence - Map executionSettings = settingsFromFunctionSettings( - function); + if (function == null) { + return selectAIService(serviceType, + arguments != null ? arguments.getExecutionSettings() : null); + } + + return selectAIService(serviceType, function.getExecutionSettings()); + } + + private AIServiceSelection selectAIService( + Class serviceType, + @Nullable Map executionSettings) { if (executionSettings == null || executionSettings.isEmpty()) { AIService service = getAnyService(serviceType); @@ -155,7 +155,7 @@ public AIServiceSelection trySelectAIService( return castServiceSelection(new AIServiceSelection<>(service, settings)); } - LOGGER.warn("No service found meeting requirements"); + LOGGER.warn(SemanticKernelResources.getString("no.service.found.meeting.requirements")); return null; } @@ -211,7 +211,8 @@ public T getService(Class clazz) { (clazz.equals(TextGenerationService.class) || clazz.equals(ChatCompletionService.class))) { LOGGER.warn( - "Requested a non-existent service type of {}. Consider requesting a TextAIService instead.", + SemanticKernelResources.getString( + "requested.a.non.existent.service.type.of.consider.requesting.a.textaiservice.instead"), clazz.getName()); } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/StreamingKernelContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/StreamingKernelContent.java index 6c1ddab10..dd1dcffe5 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/StreamingKernelContent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/StreamingKernelContent.java @@ -1,46 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.services; -import com.microsoft.semantickernel.contextvariables.ContextVariable; -import java.util.Map; -import javax.annotation.Nullable; - /** - * Base class which represents the content returned by a streaming AI service. - * + * Base class which represents the content returned by an AI service. * @param The type of the content. */ -public abstract class StreamingKernelContent extends KernelContent { - - /** - * In a scenario of multiple choices per request, this represents the zero-based index of the - * choice in the streaming sequence - */ - private final int choiceIndex; - - /** - * Initializes a new instance of the {@link StreamingKernelContent} class. - * - * @param innerContent The inner content representation. - * @param choiceIndex The zero-based index of the choice in the streaming sequence. - * @param modelId The model identifier used to generate the content. - * @param metadata The metadata associated with the content. - */ - protected StreamingKernelContent( - @Nullable T innerContent, - int choiceIndex, - @Nullable String modelId, - @Nullable Map> metadata) { - super(innerContent, modelId, null); - this.choiceIndex = choiceIndex; - } +public interface StreamingKernelContent extends KernelContent { - /** - * Gets the zero-based index of the choice in the streaming sequence. - * - * @return The zero-based index of the choice in the streaming sequence. - */ - public int getChoiceIndex() { - return choiceIndex; - } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/StreamingTextContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/StreamingTextContent.java new file mode 100644 index 000000000..95e784491 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/StreamingTextContent.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.services; + +import com.microsoft.semantickernel.contextvariables.ContextVariable; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Base class which represents the content returned by a streaming AI service. + * + * @param The type of the content. + */ +public abstract class StreamingTextContent extends KernelContentImpl implements + StreamingKernelContent { + + /** + * In a scenario of multiple choices per request, this represents the zero-based index of the + * choice in the streaming sequence + */ + private final int choiceIndex; + + /** + * Initializes a new instance of the {@link StreamingTextContent} class. + * + * @param innerContent The inner content representation. + * @param choiceIndex The zero-based index of the choice in the streaming sequence. + * @param modelId The model identifier used to generate the content. + * @param metadata The metadata associated with the content. + */ + protected StreamingTextContent( + @Nullable T innerContent, + int choiceIndex, + @Nullable String modelId, + @Nullable Map> metadata) { + super(innerContent, modelId, null); + this.choiceIndex = choiceIndex; + } + + /** + * Gets the zero-based index of the choice in the streaming sequence. + * + * @return The zero-based index of the choice in the streaming sequence. + */ + public int getChoiceIndex() { + return choiceIndex; + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/TextAIService.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/TextAIService.java index 09b3ea1fe..3eee32d36 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/TextAIService.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/TextAIService.java @@ -29,5 +29,5 @@ public interface TextAIService extends AIService { * future and/or made configurable should need arise. *

*/ - int MAXIMUM_INFLIGHT_AUTO_INVOKES = 5; + int MAXIMUM_INFLIGHT_AUTO_INVOKES = 128; } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/audio/AudioToTextService.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/audio/AudioToTextService.java index 871d4cb4f..deeec0f64 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/audio/AudioToTextService.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/audio/AudioToTextService.java @@ -24,6 +24,10 @@ Mono getTextContentsAsync( AudioContent content, @Nullable AudioToTextExecutionSettings executionSettings); + /** + * Builder for the AudioToTextService. + * @return The builder. + */ static Builder builder() { return ServiceLoadUtil.findServiceLoader(Builder.class, "com.microsoft.semantickernel.aiservices.openai.audio.OpenAiAudioToTextService$Builder") diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/audio/TextToAudioExecutionSettings.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/audio/TextToAudioExecutionSettings.java index c367e37f8..f85659c54 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/audio/TextToAudioExecutionSettings.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/audio/TextToAudioExecutionSettings.java @@ -126,6 +126,9 @@ public TextToAudioExecutionSettings build() { } return new TextToAudioExecutionSettings(voice, responseFormat, speed); } + + private Builder() { + } } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatCompletionService.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatCompletionService.java index 0e566f3dc..d1655c05d 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatCompletionService.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatCompletionService.java @@ -2,13 +2,11 @@ package com.microsoft.semantickernel.services.chatcompletion; import com.microsoft.semantickernel.Kernel; -import com.microsoft.semantickernel.builders.SemanticKernelBuilder; -import com.microsoft.semantickernel.implementation.ServiceLoadUtil; import com.microsoft.semantickernel.orchestration.InvocationContext; import com.microsoft.semantickernel.services.TextAIService; -import com.microsoft.semantickernel.services.openai.OpenAiServiceBuilder; import java.util.List; import javax.annotation.Nullable; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -42,4 +40,32 @@ Mono>> getChatMessageContentsAsync( String prompt, @Nullable Kernel kernel, @Nullable InvocationContext invocationContext); + + /** + * Gets the chat message contents asynchronously using {@code ChatHistory} to support a + * turn-based conversation. Typically, the resulting chat message contents is appended to the + * {@code chatHistory} to continue the conversation. + * + * @param chatHistory the chat history + * @param kernel the kernel + * @param invocationContext the invocation context + * @return the chat message contents + */ + Flux> getStreamingChatMessageContentsAsync( + ChatHistory chatHistory, + @Nullable Kernel kernel, + @Nullable InvocationContext invocationContext); + + /** + * Gets the chat message contents asynchronously using a prompt. + * + * @param prompt the prompt + * @param kernel the kernel + * @param invocationContext the invocation context + * @return the chat message contents + */ + Flux> getStreamingChatMessageContentsAsync( + String prompt, + @Nullable Kernel kernel, + @Nullable InvocationContext invocationContext); } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java index 8d6bdce6d..df5f18322 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java @@ -33,7 +33,7 @@ public ChatHistory() { * @param instructions The instructions to add to the chat history */ public ChatHistory(@Nullable String instructions) { - this.chatMessageContents = new ArrayList<>(); + this.chatMessageContents = Collections.synchronizedList(new ArrayList<>()); if (instructions != null) { this.chatMessageContents.add( ChatMessageTextContent.systemMessage(instructions)); @@ -45,8 +45,9 @@ public ChatHistory(@Nullable String instructions) { * * @param chatMessageContents The chat message contents to add to the chat history */ - public ChatHistory(List chatMessageContents) { - this.chatMessageContents = new ArrayList(chatMessageContents); + public ChatHistory(List> chatMessageContents) { + this.chatMessageContents = Collections + .synchronizedList(new ArrayList<>(chatMessageContents)); } /** @@ -55,7 +56,7 @@ public ChatHistory(List chatMessageContents) { * @return List of messages in the chat */ public List> getMessages() { - return Collections.unmodifiableList(chatMessageContents); + return Collections.unmodifiableList(new ArrayList<>(chatMessageContents)); } /** @@ -67,7 +68,8 @@ public Optional> getLastMessage() { if (chatMessageContents.isEmpty()) { return Optional.empty(); } - return Optional.of(chatMessageContents.get(chatMessageContents.size() - 1)); + return Optional + .of(chatMessageContents.get(chatMessageContents.size() - 1)); } /** @@ -81,6 +83,7 @@ public void addAll(ChatHistory value) { /** * Create an {@code Iterator} from the chat history. + * @return An {@code Iterator} from the chat history. */ @Override public Iterator> iterator() { @@ -99,6 +102,7 @@ public void forEach(Consumer> action) { /** * Create a {@code Spliterator} from the chat history + * @return A {@code Spliterator} from the chat history */ @Override public Spliterator> spliterator() { @@ -112,9 +116,10 @@ public Spliterator> spliterator() { * @param content The content of the message * @param encoding The encoding of the message * @param metadata The metadata of the message + * @return {@code this} ChatHistory */ public ChatHistory addMessage(AuthorRole authorRole, String content, Charset encoding, - FunctionResultMetadata metadata) { + FunctionResultMetadata metadata) { chatMessageContents.add( ChatMessageTextContent.builder() .withAuthorRole(authorRole) @@ -130,6 +135,7 @@ public ChatHistory addMessage(AuthorRole authorRole, String content, Charset enc * * @param authorRole The role of the author of the message * @param content The content of the message + * @return {@code this} ChatHistory */ public ChatHistory addMessage(AuthorRole authorRole, String content) { chatMessageContents.add( @@ -144,6 +150,7 @@ public ChatHistory addMessage(AuthorRole authorRole, String content) { * Add a message to the chat history * * @param content The content of the message + * @return {@code this} ChatHistory */ public ChatHistory addMessage(ChatMessageContent content) { chatMessageContents.add(content); @@ -154,6 +161,7 @@ public ChatHistory addMessage(ChatMessageContent content) { * Add a user message to the chat history * * @param content The content of the user message + * @return {@code this} ChatHistory */ public ChatHistory addUserMessage(String content) { return addMessage(AuthorRole.USER, content); @@ -163,6 +171,7 @@ public ChatHistory addUserMessage(String content) { * Add an assistant message to the chat history * * @param content The content of the assistant message + * @return {@code this} ChatHistory */ public ChatHistory addAssistantMessage(String content) { return addMessage(AuthorRole.ASSISTANT, content); @@ -172,11 +181,24 @@ public ChatHistory addAssistantMessage(String content) { * Add an system message to the chat history * * @param content The content of the system message + * @return {@code this} ChatHistory */ public ChatHistory addSystemMessage(String content) { return addMessage(AuthorRole.SYSTEM, content); } + /** + * Clear the chat history + */ + public void clear() { + chatMessageContents.clear(); + } + + /** + * Add all messages to the chat history + * @param messages The messages to add to the chat history + * @return {@code this} ChatHistory + */ public ChatHistory addAll(List> messages) { chatMessageContents.addAll(messages); return this; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatMessageContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatMessageContent.java index 215357911..2d4ce4911 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatMessageContent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatMessageContent.java @@ -3,6 +3,7 @@ import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; import com.microsoft.semantickernel.services.KernelContent; +import com.microsoft.semantickernel.services.KernelContentImpl; import com.microsoft.semantickernel.services.chatcompletion.message.ChatMessageContentType; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -21,8 +22,7 @@ * * @param the type of the inner content within the messages */ -public class ChatMessageContent extends KernelContent { - +public class ChatMessageContent extends KernelContentImpl { private final AuthorRole authorRole; @Nullable private final String content; @@ -51,6 +51,28 @@ public ChatMessageContent( null); } + /** + * Creates a new instance of the {@link ChatMessageContent} class. Defaults to + * {@link ChatMessageContentType#TEXT} content type. + * + * @param authorRole the author role that generated the content + * @param authorName the author name + * @param content the content + */ + public ChatMessageContent( + AuthorRole authorRole, + String authorName, + String content) { + this( + authorRole, + authorName, + content, + null, + null, + null, + null); + } + /** * Creates a new instance of the {@link ChatMessageContent} class. Defaults to * {@link ChatMessageContentType#TEXT} content type. @@ -113,7 +135,7 @@ public ChatMessageContent( */ public ChatMessageContent( AuthorRole authorRole, - @Nullable List> items, + @Nullable List> items, String modelId, T innerContent, Charset encoding, @@ -131,6 +153,36 @@ public ChatMessageContent( this.contentType = contentType; } + /** + * Creates a new instance of the {@link ChatMessageContent} class. + * + * @param authorRole the author role that generated the content + * @param items the items + * @param modelId the model id + * @param innerContent the inner content + * @param encoding the encoding + * @param metadata the metadata + */ + public ChatMessageContent( + AuthorRole authorRole, + @Nullable String content, + @Nullable List> items, + String modelId, + T innerContent, + Charset encoding, + FunctionResultMetadata metadata) { + super(innerContent, modelId, metadata); + this.content = content; + this.authorRole = authorRole; + this.encoding = encoding != null ? encoding : StandardCharsets.UTF_8; + if (items == null) { + this.items = null; + } else { + this.items = new ArrayList<>(items); + } + this.contentType = ChatMessageContentType.TEXT; + } + /** * Gets the author role that generated the content * diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/StreamingChatContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/StreamingChatContent.java new file mode 100644 index 000000000..52bb4131b --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/StreamingChatContent.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.services.chatcompletion; + +import com.microsoft.semantickernel.services.StreamingKernelContent; + +/** + * Base class which represents the content returned by a chat completion service. + * @param The type of the content. + */ +public interface StreamingChatContent extends StreamingKernelContent { + + /** + * Gets the ID of the content. + * @return The ID. + */ + public String getId(); +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/message/ChatMessageImageContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/message/ChatMessageImageContent.java index e87f7fcc1..75ab0a3dd 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/message/ChatMessageImageContent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/message/ChatMessageImageContent.java @@ -9,10 +9,21 @@ import java.util.Base64; import javax.annotation.Nullable; +/** + * Represents an image content in a chat message. + * + * @param the type of the inner content within the message + */ public class ChatMessageImageContent extends ChatMessageContent { private final ImageDetail detail; + /** + * Create a new instance of the {@link ChatMessageImageContent} class. + * @param content The chat message content + * @param modelId The LLM id to use for the chat + * @param detail The detail level of the image to include in the chat message + */ public ChatMessageImageContent( String content, @Nullable String modelId, @@ -69,6 +80,10 @@ public static Builder builder() { return new Builder<>(); } + /** + * Builder for the {@link ChatMessageImageContent} class. + * @param the type of the inner content within the message + */ public static class Builder implements SemanticKernelBuilder> { @Nullable @@ -82,7 +97,7 @@ public static class Builder implements SemanticKernelBuilder withModelId(String modelId) { this.modelId = modelId; @@ -90,10 +105,11 @@ public Builder withModelId(String modelId) { } /** + * Set the image content to include in the chat message. * @param imageType For instance jpg or png. For known types known to OpenAI see: docs. * @param content the image content - * @return this builder + * @return {@code this} builder */ public Builder withImage( String imageType, @@ -108,7 +124,7 @@ public Builder withImage( * Set the URL of the image to include in the chat message. * * @param url the URL of the image - * @return this builder + * @return {@code this} builder */ public Builder withImageUrl(String url) { this.content = url; @@ -119,7 +135,7 @@ public Builder withImageUrl(String url) { * Set the URL of the image to include in the chat message. * * @param url the URL of the image - * @return this builder + * @return {@code this} builder */ public Builder withImageUrl(URL url) { this.content = url.toString(); @@ -130,7 +146,7 @@ public Builder withImageUrl(URL url) { * Set the detail level of the image to include in the chat message. * * @param detail the detail level of the image - * @return this builder + * @return {@code this} builder */ public Builder withDetail(ImageDetail detail) { this.detail = detail; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/message/ChatMessageTextContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/message/ChatMessageTextContent.java index ccce7e4d9..f065ed2ff 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/message/ChatMessageTextContent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/message/ChatMessageTextContent.java @@ -81,6 +81,9 @@ public static ChatMessageTextContent systemMessage(String content) { return buildContent(AuthorRole.SYSTEM, content); } + /** + * Builder for the {@link ChatMessageTextContent} class. + */ public static class Builder implements SemanticKernelBuilder { @Nullable diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/TextContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/TextContent.java index 50295a7c9..3cca884dd 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/TextContent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/TextContent.java @@ -2,13 +2,13 @@ package com.microsoft.semantickernel.services.textcompletion; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; -import com.microsoft.semantickernel.services.KernelContent; +import com.microsoft.semantickernel.services.KernelContentImpl; import javax.annotation.Nullable; /** * Content from a text completion service. */ -public class TextContent extends KernelContent { +public class TextContent extends KernelContentImpl { private final String content; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/TextGenerationService.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/TextGenerationService.java index 0ab08f5f0..39fdd5149 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/TextGenerationService.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/TextGenerationService.java @@ -5,6 +5,7 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.implementation.ServiceLoadUtil; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.services.StreamingTextContent; import com.microsoft.semantickernel.services.TextAIService; import com.microsoft.semantickernel.services.openai.OpenAiServiceBuilder; import java.util.List; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/Embedding.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/Embedding.java deleted file mode 100644 index 0a11e8691..000000000 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/Embedding.java +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.services.textembedding; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** Represents a strongly typed vector of numeric data. */ -public class Embedding { - - // vector is immutable! - private final List vector; - - private static final Embedding EMPTY = new Embedding(); - - public static Embedding empty() { - return EMPTY; - } - - /** Initializes a new instance of the Embedding class. */ - public Embedding() { - this.vector = Collections.emptyList(); - } - - /** - * Initializes a new instance of the Embedding class that contains numeric elements copied from - * the specified collection - * - * @param vector The collection whose elements are copied to the new Embedding - */ - public Embedding(@Nonnull List vector) { - Objects.requireNonNull(vector); - this.vector = Collections.unmodifiableList(vector); - } - - /** - * Initializes a new instance of the Embedding class that contains numeric elements copied from - * the specified array - * - * @param vector The array whose elements are copied to the new Embedding - */ - public Embedding(@Nonnull float[] vector) { - Objects.requireNonNull(vector); - List list = new ArrayList<>(vector.length); - for (float f : vector) { - list.add(f); - } - this.vector = Collections.unmodifiableList(list); - } - - /** - * Return the embedding vector as a read-only list. - * - * @return The embedding vector as a read-only list. - */ - public List getVector() { - return Collections.unmodifiableList(this.vector); - } - - /** - * Calculates the dot product of this {@code Embedding} with another. - * - * @param other The other {@code Embedding} to compute the dot product with - * @return The dot product between the {@code Embedding} vectors - */ - public float dot(@Nonnull Embedding other) { - Objects.requireNonNull(other); - return VectorOperations.dot(this.vector, other.getVector()); - } - - /** - * Calculates the Euclidean length of this vector. - * - * @return Euclidean length - */ - public float euclideanLength() { - return VectorOperations.euclideanLength(this.vector); - } - - /** - * Calculates the cosine similarity of this vector with another. - * - * @param other The other vector to compute cosine similarity with. - * @return Cosine similarity between vectors - */ - public float cosineSimilarity(@Nonnull Embedding other) { - Objects.requireNonNull(other); - return VectorOperations.cosineSimilarity(this.vector, other.getVector()); - } - - /** - * Multiply the {@code Embedding} vector by a multiplier. - * - * @param multiplier The multiplier to multiply the {@code Embedding} vector by - * @return A new {@code Embedding} with the vector multiplied by the multiplier - */ - public Embedding multiply(float multiplier) { - return new Embedding(VectorOperations.multiply(this.vector, multiplier)); - } - - /** - * Divide the {@code Embedding} vector by a divisor. - * - * @param divisor The divisor to divide the {@code Embedding} vector by - * @return A new {@code Embedding} with the vector divided by the divisor - */ - public Embedding divide(float divisor) { - return new Embedding(VectorOperations.divide(this.vector, divisor)); - } - - /** - * Normalizes the underlying vector, such that the Euclidean length is 1. - * - * @return A new {@code Embedding} with the normalized vector - */ - public Embedding normalize() { - return new Embedding(VectorOperations.normalize(this.vector)); - } -} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/VectorOperations.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/VectorOperations.java deleted file mode 100644 index ea0b77e00..000000000 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textembedding/VectorOperations.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.services.textembedding; - -import javax.annotation.Nonnull; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -final class VectorOperations { - - /** - * Calculates the cosine similarity of two vectors. The vectors must be equal in length and have - * non-zero norm. - * - * @param x First vector, which is not modified - * @param y Second vector, which is not modified - * @return The cosine similarity of the two vectors - */ - static float cosineSimilarity(@Nonnull List x, @Nonnull List y) { - Objects.requireNonNull(x); - Objects.requireNonNull(y); - - if (x.size() != y.size()) { - throw new IllegalArgumentException("Vectors lengths must be equal"); - } - - float dotProduct = dot(x, y); - float normX = dot(x, x); - float normY = dot(y, y); - - if (normX == 0 || normY == 0) { - throw new IllegalArgumentException("Vectors cannot have zero norm"); - } - - return dotProduct / (float) (Math.sqrt(normX) * Math.sqrt(normY)); - } - - /** - * Divides the elements of the vector by the divisor. - * - * @param vector Vector to divide, which is not modified - * @param divisor Divisor to apply to each element of the vector - * @return A new vector with the elements divided by the divisor - */ - static List divide(@Nonnull List vector, float divisor) { - Objects.requireNonNull(vector); - if (Float.isNaN(divisor)) { - throw new IllegalArgumentException("Divisor cannot be NaN"); - } - if (divisor == 0f) { - throw new IllegalArgumentException("Divisor cannot be zero"); - } - - return vector.stream().map(x -> x / divisor).collect(Collectors.toList()); - } - - static float dot(@Nonnull List x, @Nonnull List y) { - Objects.requireNonNull(x); - Objects.requireNonNull(y); - - if (x.size() != y.size()) { - throw new IllegalArgumentException("Vectors lengths must be equal"); - } - - float result = 0; - for (int i = 0; i < x.size(); ++i) { - result += x.get(i) * y.get(i); - } - - return result; - } - - /** - * Calculates the Euclidean length of a vector. - * - * @param vector Vector to calculate the length of, which is not modified - * @return The Euclidean length of the vector - */ - static float euclideanLength(@Nonnull List vector) { - Objects.requireNonNull(vector); - return (float) Math.sqrt(dot(vector, vector)); - } - - /** - * Multiplies the elements of the vector by the multiplier. - * - * @param vector Vector to multiply, which is not modified - * @param multiplier Multiplier to apply to each element of the vector - * @return A new vector with the elements multiplied by the multiplier - */ - static List multiply(@Nonnull List vector, float multiplier) { - Objects.requireNonNull(vector); - if (Float.isNaN(multiplier)) { - throw new IllegalArgumentException("Multiplier cannot be NaN"); - } - if (Float.isInfinite(multiplier)) { - throw new IllegalArgumentException("Multiplier cannot be infinite"); - } - - return vector.stream().map(x -> x * multiplier).collect(Collectors.toList()); - } - - /** - * Normalizes the vector such that the Euclidean length is 1. - * - * @param vector Vector to normalize, which is not modified - * @return A new, normalized vector - */ - static List normalize(@Nonnull List vector) { - Objects.requireNonNull(vector); - return divide(vector, euclideanLength(vector)); - } -} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplate.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplate.java index 2e7c260e6..bd8df1383 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplate.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplate.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.templateengine.handlebars; -import static com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments.MAIN_KEY; +import static com.microsoft.semantickernel.semanticfunctions.KernelArguments.MAIN_KEY; import com.github.jknack.handlebars.Context; import com.github.jknack.handlebars.EscapingStrategy; @@ -9,6 +9,7 @@ import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; import com.github.jknack.handlebars.ValueResolver; +import com.github.jknack.handlebars.context.JavaBeanValueResolver; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.contextvariables.ContextVariable; import com.microsoft.semantickernel.contextvariables.ContextVariableType; @@ -17,17 +18,16 @@ import com.microsoft.semantickernel.orchestration.InvocationContext; import com.microsoft.semantickernel.orchestration.ToolCallBehavior; import com.microsoft.semantickernel.plugin.KernelPlugin; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateOption; import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.Locale; import java.util.Map.Entry; import java.util.Optional; @@ -58,7 +58,7 @@ public HandlebarsPromptTemplate( @Override public Mono renderAsync( Kernel kernel, - @Nullable KernelFunctionArguments arguments, + @Nullable KernelArguments arguments, @Nullable InvocationContext context) { String template = promptTemplate.getTemplate(); if (template == null) { @@ -74,7 +74,7 @@ public Mono renderAsync( template, context); if (arguments == null) { - arguments = KernelFunctionArguments.builder().build(); + arguments = KernelArguments.builder().build(); } return handler.render(arguments); } @@ -88,8 +88,7 @@ public Object resolve(Object context, String name) { if ("role".equalsIgnoreCase(name)) { return ((ChatMessageContent) context).getAuthorRole().name(); } else if ("content".equalsIgnoreCase(name)) { - return ContextVariableTypeConverter - .escapeXmlString(((ChatMessageContent) context).getContent()); + return ((ChatMessageContent) context).getContent(); } } return UNRESOLVED; @@ -130,17 +129,24 @@ private static class ContextVariableResolver implements ValueResolver { @Override public Object resolve(Object context, String name) { Object value = null; - if (context instanceof KernelFunctionArguments) { - ContextVariable variable = ((KernelFunctionArguments) context).get(name); - value = variable != null ? variable.getValue() : UNRESOLVED; - } - if (context instanceof ContextVariable) { - value = ((ContextVariable) context).getValue(); + ContextVariable variable = null; + if (context instanceof KernelArguments) { + variable = ((KernelArguments) context).get(name); + } else if (context instanceof ContextVariable) { + variable = ((ContextVariable) context); } - if (value == null) { + + if (variable == null || variable.getValue() == null) { return UNRESOLVED; - } else { + } + + value = variable.getValue(); + + if (value instanceof Iterable) { return value; + } else { + // It is likely this will come escaped, but will be re escaped by the handlebars engine + return promptString(variable); } } @@ -148,16 +154,28 @@ public Object resolve(Object context, String name) { public Object resolve(Object context) { if (context instanceof ContextVariable) { Object result = ((ContextVariable) context).getValue(); - return result != null ? result : UNRESOLVED; + + if (result == null) { + return UNRESOLVED; + } else if (result instanceof Iterable) { + return result; + } else { + return promptString(((ContextVariable) context)); + } } return UNRESOLVED; } + private String promptString(ContextVariable context) { + // This will come escaped, but will be re escaped by the handlebars engine + return StringEscapeUtils.unescapeXml(context.toPromptString()); + } + @Override public Set> propertySet(Object context) { - if (context instanceof KernelFunctionArguments) { + if (context instanceof KernelArguments) { HashMap result = new HashMap<>(); - result.putAll((KernelFunctionArguments) context); + result.putAll((KernelArguments) context); return result.entrySet(); } else if (context instanceof ContextVariable) { HashMap result = new HashMap<>(); @@ -168,12 +186,11 @@ public Set> propertySet(Object context) { } } - private static class HandleBarsPromptTemplateHandler { + private class HandleBarsPromptTemplateHandler { private final String template; private final Handlebars handlebars; - @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") // Think this is a false positive public HandleBarsPromptTemplateHandler( Kernel kernel, String template, @@ -181,7 +198,7 @@ public HandleBarsPromptTemplateHandler( this.template = template; this.handlebars = new Handlebars(); this.handlebars - .registerHelper("message", HandleBarsPromptTemplateHandler::handleMessage) + .registerHelper("message", this::handleMessage) .registerHelper("each", handleEach(context)) .with(EscapingStrategy.XML); @@ -190,23 +207,20 @@ public HandleBarsPromptTemplateHandler( // TODO: 1.0 Add more helpers } - private static Helper handleEach(InvocationContext invocationContext) { - return (context, options) -> { - if (context instanceof ContextVariable) { - return ((ContextVariable) context) + private Helper handleEach(InvocationContext invocationContext) { + return (variable, options) -> { + if (variable instanceof ContextVariable) { + return ((ContextVariable) variable) .toPromptString(invocationContext.getContextVariableTypes()); } - if (context instanceof Iterable) { + if (variable instanceof Iterable) { StringBuilder sb = new StringBuilder(); - Iterator iterator = ((Iterable) context).iterator(); - while (iterator.hasNext()) { - Object element = iterator.next(); + + for (Object element : (Iterable) variable) { if (element instanceof KernelPlugin) { KernelPlugin plugin = (KernelPlugin) element; - Iterator> functions = plugin.iterator(); - while (functions.hasNext()) { - KernelFunction function = functions.next(); + for (KernelFunction function : plugin) { sb.append(options.fn(function)); } } else { @@ -217,17 +231,18 @@ private static Helper handleEach(InvocationContext invocationContext) { } ContextVariableType type = invocationContext.getContextVariableTypes() - .getVariableTypeForClass(context.getClass()); + .getVariableTypeForClass(variable.getClass()); if (type != null) { - return type.getConverter() - .toPromptString(invocationContext.getContextVariableTypes(), context); + return type + .getConverter() + .toPromptString(invocationContext.getContextVariableTypes(), variable); } return null; }; } @Nullable - private static CharSequence handleMessage(Object context, Options options) + private CharSequence handleMessage(Object context, Options options) throws IOException { String role = options.hash("role"); String content = (String) options.fn(context); @@ -247,18 +262,22 @@ private static CharSequence handleMessage(Object context, Options options) return new Handlebars.SafeString( String.format( "%s", - role.toLowerCase(Locale.ROOT), content)); + role.toLowerCase(Locale.ROOT), + content)); } return null; } - public Mono render(KernelFunctionArguments variables) { + public Mono render(KernelArguments variables) { try { ArrayList resolvers = new ArrayList<>(); resolvers.add(new MessageResolver()); resolvers.add(new ContextVariableResolver()); - // resolvers.addAll(ValueResolver.defaultValueResolvers()); + if (promptTemplate.getPromptTemplateOptions() + .contains(PromptTemplateOption.ALLOW_CONTEXT_VARIABLE_METHOD_CALLS_UNSAFE)) { + resolvers.add(JavaBeanValueResolver.INSTANCE); + } Context context = Context .newBuilder(variables) @@ -298,9 +317,9 @@ private static Helper functionInvokeHelper( InvocationContext invocationContext) { return (context, options) -> { - KernelFunctionArguments.Builder builder = KernelFunctionArguments.builder(); - if (context instanceof KernelFunctionArguments) { - builder.withVariables((KernelFunctionArguments) context); + KernelArguments.Builder builder = KernelArguments.builder(); + if (context instanceof KernelArguments) { + builder.withVariables((KernelArguments) context); } else { builder.withInput(context); } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/semantickernel/TemplateException.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/semantickernel/TemplateException.java index 6c5779b42..e0fe492c1 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/semantickernel/TemplateException.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/semantickernel/TemplateException.java @@ -2,6 +2,7 @@ package com.microsoft.semantickernel.templateengine.semantickernel; import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.localization.SemanticKernelResources; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -78,34 +79,39 @@ public enum ErrorCodes { * Unknown error. */ //spotless:on - UNKNOWN_ERROR("Unknown error"), + UNKNOWN_ERROR(SemanticKernelResources.getString("unknown.error")), // spotless:off /** * Syntax error, the template syntax used is not valid. */ //spotless:on - SYNTAX_ERROR("Syntax error, the template syntax used is not valid"), + SYNTAX_ERROR( + SemanticKernelResources + .getString("syntax.error.the.template.syntax.used.is.not.valid")), // spotless:off /** * The block type produced be the tokenizer was not expected. */ - UNEXPECTED_BLOCK_TYPE("The block type produced be the tokenizer was not expected"), + UNEXPECTED_BLOCK_TYPE(SemanticKernelResources.getString( + "the.block.type.produced.be.the.tokenizer.was.not.expected")), //spotless:off /** * The template requires an unknown function. */ //spotless:on - FUNCTION_NOT_FOUND("The template requires an unknown function"), + FUNCTION_NOT_FOUND( + SemanticKernelResources.getString("the.template.requires.an.unknown.function")), // spotless:off /** * The template execution failed, e.g. a function call threw an exception. */ //spotless:on - RUNTIME_ERROR("The template execution failed, e.g. a function call threw an exception"); + RUNTIME_ERROR(SemanticKernelResources.getString( + "the.template.execution.failed.e.g.a.function.call.threw.an.exception")); private final String message; diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/data/Hotel.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/data/Hotel.java new file mode 100644 index 000000000..f6741e5a5 --- /dev/null +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/data/Hotel.java @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey; +import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; + +import java.util.List; + +public class Hotel { + @VectorStoreRecordKey + private final String id; + + @VectorStoreRecordData(isFilterable = true) + private final String name; + + @VectorStoreRecordData + private final int code; + + @JsonProperty("summary") + @VectorStoreRecordData() + private final String description; + + @JsonProperty("summaryEmbedding1") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.EUCLIDEAN_DISTANCE) + private final List euclidean; + + @JsonProperty("summaryEmbedding2") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE) + private final List cosineDistance; + + @JsonProperty("summaryEmbedding3") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_SIMILARITY) + private final List cosineSimilarity; + + @JsonProperty("summaryEmbedding4") + @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.DOT_PRODUCT) + private final List dotProduct; + @VectorStoreRecordData + private double rating; + + public Hotel() { + this(null, null, 0, null, null, null, null, null, 0.0); + } + + @JsonCreator + public Hotel( + @JsonProperty("id") String id, + @JsonProperty("name") String name, + @JsonProperty("code") int code, + @JsonProperty("summary") String description, + @JsonProperty("summaryEmbedding1") List euclidean, + @JsonProperty("summaryEmbedding2") List cosineDistance, + @JsonProperty("summaryEmbedding2") List cosineSimilarity, + @JsonProperty("summaryEmbedding3") List dotProduct, + @JsonProperty("rating") double rating) { + this.id = id; + this.name = name; + this.code = code; + this.description = description; + this.euclidean = euclidean; + this.cosineDistance = euclidean; + this.cosineSimilarity = euclidean; + this.dotProduct = euclidean; + this.rating = rating; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + public List getEuclidean() { + return euclidean; + } + + public List getCosineDistance() { + return cosineDistance; + } + + public List getDotProduct() { + return dotProduct; + } + + public double getRating() { + return rating; + } + + public void setRating(double rating) { + this.rating = rating; + } +} diff --git a/semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionTest.java similarity index 55% rename from semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionTest.java rename to semantickernel-api/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionTest.java index 9a087adbb..bd2c633d6 100644 --- a/semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreRecordCollectionTest.java @@ -1,19 +1,24 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.data; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter; +import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult; +import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction; +import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; public class VolatileVectorStoreRecordCollectionTest { @@ -35,20 +40,35 @@ public void clearCollection() { recordCollection.createCollectionAsync().block(); } - private List getHotels() { + private static List getHotels() { return Arrays.asList( - new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", Arrays.asList(1.0f, 2.0f, 3.0f), + new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", + Arrays.asList(0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f), null, null, null, + 4.0), + new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", + Arrays.asList(-2.0f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f), null, null, null, 4.0), - new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", Arrays.asList(1.0f, 2.0f, 3.0f), - 3.0), - new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", Arrays.asList(1.0f, 2.0f, 3.0f), + new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", + Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.3f), null, null, null, 5.0), - new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", Arrays.asList(1.0f, 2.0f, 3.0f), + new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", + Arrays.asList(7.0f, 1.2f, -5.3f, 2.5f, 6.6f, -7.8f, 3.9f, -0.1f), null, null, null, 4.0), - new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", Arrays.asList(1.0f, 2.0f, 3.0f), - 5.0)); + new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", + Arrays.asList(-3.5f, 4.4f, -1.2f, 9.9f, 5.7f, -6.1f, 7.8f, -2.0f), null, null, null, + 4.0)); } + /** + * Search embeddings similar to the third hotel embeddings. + * In order of similarity: + * 1. Hotel 3 + * 2. Hotel 1 + * 3. Hotel 4 + */ + private static final List SEARCH_EMBEDDINGS = Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, + -0.8f, 1.1f, -2.2f, 8.2f); + @Test public void createAndDeleteCollectionAsync() { assertEquals(true, recordCollection.collectionExistsAsync().block()); @@ -128,4 +148,60 @@ public void deleteBatchAsync() { assertNull(recordCollection.getAsync(key, null).block()); } } + + @ParameterizedTest + @EnumSource(DistanceFunction.class) + public void exactSearch(DistanceFunction distanceFunction) { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(distanceFunction.getValue()) + .withTop(3) + .build(); + + // Embeddings similar to the third hotel + List> results = recordCollection + .searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The third hotel should be the most similar + assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId()); + + options = VectorSearchOptions.builder() + .withVectorFieldName(distanceFunction.getValue()) + .withSkip(1) + .withTop(-100) + .build(); + + // Skip the first result + results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(1, results.size()); + // The first hotel should be the most similar + assertEquals(hotels.get(0).getId(), results.get(0).getRecord().getId()); + } + + @ParameterizedTest + @EnumSource(DistanceFunction.class) + public void searchWithFilter(DistanceFunction distanceFunction) { + List hotels = getHotels(); + recordCollection.upsertBatchAsync(hotels, null).block(); + + VectorSearchOptions options = VectorSearchOptions.builder() + .withVectorFieldName(distanceFunction.getValue()) + .withTop(3) + .withVectorSearchFilter( + VectorSearchFilter.builder() + .equalTo("rating", 4.0).build()) + .build(); + + // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned + List> results = recordCollection + .searchAsync(SEARCH_EMBEDDINGS, options).block().getResults(); + assertNotNull(results); + assertEquals(3, results.size()); + // The first hotel should be the most similar + assertEquals(hotels.get(0).getId(), results.get(0).getRecord().getId()); + } } diff --git a/semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreTest.java similarity index 82% rename from semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreTest.java rename to semantickernel-api/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreTest.java index cfd52757a..4cee074dc 100644 --- a/semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/data/VolatileVectorStoreTest.java @@ -1,17 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.data; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.List; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + public class VolatileVectorStoreTest { + private static VolatileVectorStore vectorStore; @BeforeAll @@ -24,7 +24,11 @@ public void collectionNamesAsync() { List collectionNames = Arrays.asList("hotels1", "hotels2", "hotels3"); for (String collectionName : collectionNames) { - vectorStore.getCollection(collectionName, Hotel.class, null).createCollectionAsync() + vectorStore.getCollection(collectionName, + VolatileVectorStoreRecordCollectionOptions.builder() + .withRecordClass(Hotel.class) + .build()) + .createCollectionAsync() .block(); } diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/localization/SemanticKernelResourcesTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/localization/SemanticKernelResourcesTest.java new file mode 100644 index 000000000..9158255f7 --- /dev/null +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/localization/SemanticKernelResourcesTest.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.localization; + +import java.util.Locale; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class SemanticKernelResourcesTest { + + @Test + public void languageAndCountry() { + SemanticKernelResources.setLocale(new Locale("en", "GB")); + + String result = SemanticKernelResources.localize("test_language_country", "a test string"); + + Assertions.assertEquals("GB english test value", result); + } + + @Test + public void valueAtTheLanguageLevel() { + SemanticKernelResources.setLocale(new Locale("en", "GB")); + + String result = SemanticKernelResources.localize("test_language", "a test string"); + + Assertions.assertEquals("English test value", result); + } + + @Test + public void topLevelValue() { + SemanticKernelResources.setLocale(new Locale("en", "GB")); + + String result = SemanticKernelResources.localize("test_top", "default value"); + + Assertions.assertEquals("Top level value", result); + } + + @Test + public void defaultValue() { + SemanticKernelResources.setLocale(new Locale("en", "GB")); + + String result = SemanticKernelResources.localize("not-there", "default value"); + + Assertions.assertEquals("default value", result); + } +} diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/orchestration/PromptExecutionSettingsTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/orchestration/PromptExecutionSettingsTest.java index daec47226..9c21b1158 100644 --- a/semantickernel-api/src/test/java/com/microsoft/semantickernel/orchestration/PromptExecutionSettingsTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/orchestration/PromptExecutionSettingsTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.orchestration.responseformat.ResponseFormat.Type; import org.junit.jupiter.api.Test; public class PromptExecutionSettingsTest { @@ -113,7 +114,8 @@ void testJsonDeserializeAndBuilder() throws Exception { + "\"best_of\":3," + "\"results_per_prompt\":5," + "\"model_id\":\"custom-model\"," - + "\"user\":\"custom-user\"" + + "\"user\":\"custom-user\"," + + "\"response_format\" : {\"type\" : \"text\"}" + "}"; PromptExecutionSettings settingsFromJson = new ObjectMapper().readValue(json, PromptExecutionSettings.class); @@ -129,8 +131,13 @@ void testJsonDeserializeAndBuilder() throws Exception { .withResultsPerPrompt(5) .withModelId("custom-model") .withUser("custom-user") + .withResponseFormat(Type.TEXT) .build(); - assertEquals(settingsFromBuilder, settingsFromJson); + assertEquals( + new ObjectMapper().writerWithDefaultPrettyPrinter() + .writeValueAsString(settingsFromBuilder), + new ObjectMapper().writerWithDefaultPrettyPrinter() + .writeValueAsString(settingsFromJson)); } } diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethodTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethodTest.java index 589705f33..a2dae1370 100644 --- a/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethodTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethodTest.java @@ -52,7 +52,7 @@ void typeConversionOnMethodCall() { .invokeAsync(kernel) .withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(String.class)) .withArguments( - KernelFunctionArguments.builder() + KernelArguments.builder() .withVariable("number1", "12.0") .build()) .block(); @@ -182,7 +182,7 @@ interface InvocationTest { Method getMethod() throws NoSuchMethodException; - KernelFunctionArguments getArguments(); + KernelArguments getArguments(); void assertCalled(); } @@ -203,8 +203,8 @@ public Method getMethod() throws NoSuchMethodException { } @Override - public KernelFunctionArguments getArguments() { - return KernelFunctionArguments.builder() + public KernelArguments getArguments() { + return KernelArguments.builder() .withVariable("i", 123) .build(); } @@ -232,8 +232,8 @@ public Method getMethod() throws NoSuchMethodException { } @Override - public KernelFunctionArguments getArguments() { - return KernelFunctionArguments.builder() + public KernelArguments getArguments() { + return KernelArguments.builder() .withVariable("i", 123) .build(); } @@ -261,8 +261,8 @@ public Method getMethod() throws NoSuchMethodException { } @Override - public KernelFunctionArguments getArguments() { - return KernelFunctionArguments.builder() + public KernelArguments getArguments() { + return KernelArguments.builder() .withVariable("i", 123) .build(); } @@ -290,8 +290,8 @@ public Method getMethod() throws NoSuchMethodException { } @Override - public KernelFunctionArguments getArguments() { - return KernelFunctionArguments.builder() + public KernelArguments getArguments() { + return KernelArguments.builder() .withVariable("i", Arrays.asList(1, 2, 3)) .build(); } @@ -319,8 +319,8 @@ public Method getMethod() throws NoSuchMethodException { } @Override - public KernelFunctionArguments getArguments() { - return KernelFunctionArguments.builder() + public KernelArguments getArguments() { + return KernelArguments.builder() .build(); } @@ -347,7 +347,7 @@ public Method getMethod() throws NoSuchMethodException { } @Override - public KernelFunctionArguments getArguments() { + public KernelArguments getArguments() { ContextVariableTypeConverter dbConverter = ContextVariableTypeConverter .builder(BigDecimal.class) @@ -355,7 +355,7 @@ public KernelFunctionArguments getArguments() { .toPromptString(i -> null) .build(); - return KernelFunctionArguments.builder() + return KernelArguments.builder() .withVariable("i", new BigDecimal(123), dbConverter) .build(); } @@ -401,14 +401,14 @@ public Method getMethod() throws NoSuchMethodException { } @Override - public KernelFunctionArguments getArguments() { + public KernelArguments getArguments() { ContextVariableTypeConverter sourceConverter = ContextVariableTypeConverter .builder(SourceClass.class) .fromObject(i -> (SourceClass) i) .toPromptString(i -> null) .build(); - return KernelFunctionArguments.builder() + return KernelArguments.builder() .withVariable("i", new SourceClass(123), sourceConverter) .build(); } diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionYamlTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionYamlTest.java index 513f4fd72..eb71ffec7 100644 --- a/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionYamlTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionYamlTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import java.nio.file.Path; import java.util.Arrays; @@ -78,12 +80,24 @@ private void testFromPromptYaml(String templateFormat) throws Exception { assertNotNull(result); assertEquals(expResult.getName(), result.getName()); assertEquals(expResult.getDescription(), result.getDescription()); - assertEquals(expResult.getExecutionSettings(), result.getExecutionSettings()); + assertEqualsJson(expResult.getExecutionSettings(), result.getExecutionSettings()); assertEquals(expResult.getMetadata(), result.getMetadata()); } + public static void assertEqualsJson(Object a, Object b) { + try { + assertEquals(new ObjectMapper().writerWithDefaultPrettyPrinter() + .writeValueAsString(a), + new ObjectMapper().writerWithDefaultPrettyPrinter() + .writeValueAsString(b)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + /** * Test of fromYaml method, of class KernelFunctionYaml. + * @throws Exception if parsing the YAML throws an Exception */ @Test @Disabled diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateConfigTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateConfigTest.java index d4141dcf8..387ae06b6 100644 --- a/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateConfigTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateConfigTest.java @@ -1,10 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.semanticfunctions; +import static com.microsoft.semantickernel.semanticfunctions.KernelFunctionYamlTest.assertEqualsJson; import static org.junit.jupiter.api.Assertions.assertEquals; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.Test; @@ -28,6 +30,7 @@ void testInstanceMadeWithBuilderEqualsInstanceMadeWithConstructor() { name, template, "semantic-kernel", + Collections.emptySet(), description, inputVariables, outputVariable, @@ -50,7 +53,7 @@ void testInstanceMadeWithBuilderEqualsInstanceMadeWithConstructor() { }) .build(); - assertEquals(expected, result); + assertEqualsJson(expected, result); } @Test @@ -143,6 +146,6 @@ void testParseFromJson() throws Exception { + "}"; PromptTemplateConfig result = PromptTemplateConfig.parseFromJson(jsonString); - assertEquals(expected, result); + assertEqualsJson(expected, result); } } diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateFactoryTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateFactoryTest.java index f386084e4..67e86d43d 100644 --- a/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateFactoryTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/semanticfunctions/PromptTemplateFactoryTest.java @@ -93,7 +93,7 @@ private void executeTest(String templateFormat) throws Exception { PromptTemplate promptTemplate = PromptTemplateFactory.build(config); - KernelFunctionArguments args = KernelFunctionArguments.builder() + KernelArguments args = KernelArguments.builder() .withInput(ContextVariable.of("input from args")).build(); String expected = String.format("A template for testing: %s", diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/services/AIServiceSelectorTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/services/AIServiceSelectorTest.java index f9be46cdd..13ee076f1 100644 --- a/semantickernel-api/src/test/java/com/microsoft/semantickernel/services/AIServiceSelectorTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/services/AIServiceSelectorTest.java @@ -8,7 +8,7 @@ import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; import org.junit.jupiter.api.Test; @@ -185,7 +185,7 @@ public String getServiceId() { AIServiceSelection expected = new AIServiceSelection<>(aService, null); - KernelFunctionArguments arguments = KernelFunctionArguments.builder().build(); + KernelArguments arguments = KernelArguments.builder().build(); Kernel kernel = Kernel.builder() .withAIService((Class) aService.getClass(), aService) diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplateTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplateTest.java index eec1bfe72..3a31de30e 100644 --- a/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplateTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplateTest.java @@ -5,10 +5,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import com.fasterxml.jackson.annotation.JsonProperty; import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.contextvariables.ContextVariable; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; +import com.microsoft.semantickernel.contextvariables.converters.ContextVariableJacksonConverter; +import com.microsoft.semantickernel.orchestration.InvocationContext; import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.plugin.KernelPluginFactory; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; @@ -17,6 +22,8 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import org.apache.commons.text.StringEscapeUtils; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /** @@ -90,7 +97,7 @@ void testRenderAsync() { HandlebarsPromptTemplate instance = new HandlebarsPromptTemplate(promptTemplate); - KernelFunctionArguments arguments = KernelFunctionArguments.builder() + KernelArguments arguments = KernelArguments.builder() .withVariable("input", "Hello ") .withVariable("suffix", "World") .withVariable("choices", choices) @@ -115,4 +122,141 @@ void testRenderAsync() { .collect(joining(" ")); assertEquals(expResult, normalizedResult); } + + public static class Foo { + + @JsonProperty("val") + private final String val; + + public Foo(String val) { + this.val = val; + } + + public String getVal() { + return val; + } + } + + @Test + public void testSerializesObject() { + PromptTemplateConfig promptTemplate = PromptTemplateConfig.builder() + .withTemplate("{{input}}") + .withTemplateFormat("handlebars") + .build(); + + HandlebarsPromptTemplate instance = new HandlebarsPromptTemplate(promptTemplate); + + KernelArguments arguments = KernelArguments.builder() + .withVariable("input", new Foo("bar"), + ContextVariableJacksonConverter.create(Foo.class)) + .build(); + + // Return from renderAsync is normalized to remove empty lines and leading/trailing whitespace + String expResult = StringEscapeUtils.escapeXml11("{ \"val\" : \"bar\"}"); + + String result = instance.renderAsync(Kernel.builder().build(), arguments, null) + .block(); + Assertions.assertEquals(expResult, result.replaceAll("\\r\\n|\\r|\\n", "")); + } + + @Test + public void testMessageContent() { + PromptTemplateConfig promptTemplate = PromptTemplateConfig.builder() + .withTemplate( + "{{#each input}}\n" + + "{{content}}\n" + + "{{/each}}") + .withTemplateFormat("handlebars") + .build(); + + HandlebarsPromptTemplate instance = new HandlebarsPromptTemplate(promptTemplate); + + KernelArguments arguments = KernelArguments.builder() + .withVariable("input", new ChatHistory() + .addAssistantMessage("foo") + .addUserMessage("bar\"<>&")) + .build(); + + // Return from renderAsync is normalized to remove empty lines and leading/trailing whitespace + String expResult = "foobar"<>&"; + + String result = instance.renderAsync(Kernel.builder().build(), arguments, null) + .block(); + Assertions.assertEquals(expResult, result.replaceAll("\\n", "")); + } + + @Test + public void testMessageHandler() { + PromptTemplateConfig promptTemplate = PromptTemplateConfig.builder() + .withTemplate("{{#message role=\"user\"}}\n" + + "{{input}}\n" + + "{{/message}}") + .withTemplateFormat("handlebars") + .build(); + + HandlebarsPromptTemplate instance = new HandlebarsPromptTemplate(promptTemplate); + + KernelArguments arguments = KernelArguments.builder() + .withVariable("input", "bar\"<>&") + .build(); + + // Return from renderAsync is normalized to remove empty lines and leading/trailing whitespace + String expResult = "bar"<>&"; + + String result = instance.renderAsync(Kernel.builder().build(), arguments, null) + .block(); + Assertions.assertEquals(expResult, result.replaceAll("\\n", "")); + } + + @Test + public void iterableWithContextVariable() { + PromptTemplateConfig promptTemplate = PromptTemplateConfig.builder() + .withTemplate( + "{{#each input}}" + + "{{this}}" + + "{{/each}}") + .withTemplateFormat("handlebars") + .build(); + + HandlebarsPromptTemplate instance = new HandlebarsPromptTemplate(promptTemplate); + + KernelArguments arguments = KernelArguments.builder() + .withVariable("input", Arrays.asList(ContextVariable.of("foo\"<>&"))) + .build(); + + // Return from renderAsync is normalized to remove empty lines and leading/trailing whitespace + String expResult = "foo"<>&"; + + String result = instance.renderAsync(Kernel.builder().build(), arguments, null) + .block(); + Assertions.assertEquals(expResult, result.replaceAll("\\n", "")); + } + + @Test + public void withCustomConverter() { + PromptTemplateConfig promptTemplate = PromptTemplateConfig.builder() + .withTemplate("{{#each input}}{{this}}{{/each}}") + .withTemplateFormat("handlebars") + .build(); + + HandlebarsPromptTemplate instance = new HandlebarsPromptTemplate(promptTemplate); + + ContextVariableTypeConverter converter = ContextVariableTypeConverter.builder( + Foo.class) + .toPromptString(Foo::getVal) + .build(); + KernelArguments arguments = KernelArguments.builder() + .withVariable("input", ContextVariable.of(new Foo("bar\"<>&"), converter)) + .build(); + + // Return from renderAsync is normalized to remove empty lines and leading/trailing whitespace + String expResult = "bar"<>&"; + + String result = instance.renderAsync(Kernel.builder().build(), arguments, + InvocationContext.builder() + .withContextVariableConverter(converter) + .build()) + .block(); + Assertions.assertEquals(expResult, result.replaceAll("\\n", "")); + } } \ No newline at end of file diff --git a/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/semantickernel/CodeTokenizerTest.java b/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/semantickernel/CodeTokenizerTest.java index 8a491680c..d39c6aaff 100644 --- a/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/semantickernel/CodeTokenizerTest.java +++ b/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/semantickernel/CodeTokenizerTest.java @@ -6,7 +6,7 @@ import com.microsoft.semantickernel.implementation.templateengine.tokenizer.blocks.Block; import com.microsoft.semantickernel.implementation.templateengine.tokenizer.blocks.FunctionIdBlock; import com.microsoft.semantickernel.implementation.templateengine.tokenizer.blocks.NamedArgBlock; -import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -28,7 +28,7 @@ public void parseNamedArgs1() { Assertions.assertEquals("street", namedArgBlock.getName()); Assertions.assertEquals("123 Main St", namedArgBlock.getValue( new ContextVariableTypes(), - new KernelFunctionArguments.Builder() + KernelArguments.builder() .withVariable("street", "123 Main St") .build())); @@ -36,13 +36,13 @@ public void parseNamedArgs1() { Assertions.assertEquals("zip", namedArgBlock.getName()); Assertions.assertEquals("98123", namedArgBlock.getValue( new ContextVariableTypes(), - new KernelFunctionArguments.Builder().build())); + KernelArguments.builder().build())); namedArgBlock = (NamedArgBlock) tokens.get(3); Assertions.assertEquals("city", namedArgBlock.getName()); Assertions.assertEquals("Seattle", namedArgBlock.getValue( new ContextVariableTypes(), - new KernelFunctionArguments.Builder().build())); + KernelArguments.builder().build())); } @Test @@ -60,7 +60,6 @@ public void parseNamedArgs2() { Assertions.assertEquals("recall", namedArgBlock.getName()); Assertions.assertEquals("where did I grow up?", namedArgBlock.getValue( new ContextVariableTypes(), - new KernelFunctionArguments.Builder() - .build())); + KernelArguments.builder().build())); } } diff --git a/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle.properties b/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle.properties new file mode 100644 index 000000000..6535223b2 --- /dev/null +++ b/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle.properties @@ -0,0 +1 @@ +test_top=Top level value \ No newline at end of file diff --git a/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle_en.properties b/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle_en.properties new file mode 100644 index 000000000..8f7fc02e8 --- /dev/null +++ b/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle_en.properties @@ -0,0 +1 @@ +test_language=English test value \ No newline at end of file diff --git a/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle_en_GB.properties b/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle_en_GB.properties new file mode 100644 index 000000000..9acef1680 --- /dev/null +++ b/semantickernel-api/src/test/resources/com/microsoft/semantickernel/localization/ResourceBundle_en_GB.properties @@ -0,0 +1 @@ +test_language_country=GB english test value \ No newline at end of file diff --git a/semantickernel-bom/pom.xml b/semantickernel-bom/pom.xml index 180d8da8b..d0eb376b7 100644 --- a/semantickernel-bom/pom.xml +++ b/semantickernel-bom/pom.xml @@ -5,7 +5,7 @@ com.microsoft.semantic-kernel semantickernel-bom - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT pom Semantic Kernel Java BOM @@ -13,7 +13,7 @@ https://www.github.com/microsoft/semantic-kernel - 2.17.1 + 2.22.1 @@ -21,7 +21,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.3.0 + 3.5.0 enforce-maven @@ -31,14 +31,20 @@ - 3.9.3 + 3.9.9 + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.8.0 + + @@ -46,7 +52,7 @@ io.opentelemetry opentelemetry-bom - 1.39.0 + 1.61.0 pom import @@ -90,26 +96,71 @@ semantickernel-aiservices-google ${project.version} + + com.microsoft.semantic-kernel + semantickernel-data-azureaisearch + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-data-jdbc + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-data-mysql + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-data-sqlite + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-data-hsqldb + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-data-oracle + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-data-redis + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-agents-core + ${project.version} + + + com.microsoft.semantic-kernel + semantickernel-data-postgres + ${project.version} + com.azure azure-ai-openai - 1.0.0-beta.8 + 1.0.0-beta.16 com.azure azure-identity - 1.12.2 + 1.18.2 com.azure azure-core - 1.49.1 + 1.57.1 com.azure azure-search-documents - 11.7.0-beta.3 + 11.8.1 com.azure @@ -120,13 +171,13 @@ redis.clients jedis - 5.1.0 + 7.4.1 com.fasterxml.jackson.core jackson-annotations - ${com.fasterxml.jackson.core.version} + 2.21 com.fasterxml.jackson.core @@ -146,6 +197,7 @@ ${com.fasterxml.jackson.core.version} runtime + com.github.jknack handlebars @@ -159,7 +211,7 @@ org.slf4j slf4j-api - 2.0.12 + 2.0.17 com.google.code.findbugs @@ -175,23 +227,41 @@ provided - com.github.spotbugs spotbugs-annotations - 4.8.3 + 4.9.8 org.apache.commons commons-text - 1.11.0 + 1.15.0 com.google.cloud google-cloud-vertexai - 1.1.0 + 1.52.0 + + + + com.github.victools + jsonschema-generator + 4.38.0 + true + + + com.github.victools + jsonschema-module-jackson + 4.38.0 + true + + io.projectreactor + reactor-core + 3.8.5 + + diff --git a/semantickernel-experimental/pom.xml b/semantickernel-experimental/pom.xml index 20fa172a2..63031771a 100644 --- a/semantickernel-experimental/pom.xml +++ b/semantickernel-experimental/pom.xml @@ -4,7 +4,7 @@ com.microsoft.semantic-kernel semantickernel-parent - 1.2.1-SNAPSHOT + 1.5.1-SNAPSHOT semantickernel-experimental @@ -64,7 +64,6 @@ org.wiremock wiremock - 3.3.1 test @@ -77,7 +76,6 @@ org.mockito mockito-junit-jupiter - 5.11.0 test @@ -90,6 +88,11 @@ com.microsoft.semantic-kernel semantickernel-api + + com.microsoft.semantic-kernel + semantickernel-api-builders + provided + @@ -109,6 +112,16 @@ + + org.postgresql + postgresql + 42.7.10 + + + org.xerial + sqlite-jdbc + 3.53.0.0 + @@ -117,7 +130,6 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 false 1 diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStore.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStore.java deleted file mode 100644 index 39d13f75e..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStore.java +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.azureaisearch; - -import com.azure.search.documents.indexes.SearchIndexAsyncClient; -import com.azure.search.documents.indexes.models.SearchIndex; -import com.microsoft.semantickernel.data.VectorStore; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import reactor.core.publisher.Mono; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.List; - -public class AzureAISearchVectorStore - implements VectorStore> { - - private final SearchIndexAsyncClient client; - private final AzureAISearchVectorStoreOptions options; - - /** - * Creates a new instance of {@link AzureAISearchVectorStore}. - * - * @param client The Azure AI Search client. - * @param options The options for the vector store. - */ - @SuppressFBWarnings("EI_EXPOSE_REP2") - public AzureAISearchVectorStore(@Nonnull SearchIndexAsyncClient client, - @Nonnull AzureAISearchVectorStoreOptions options) { - this.client = client; - this.options = options; - } - - /** - * Gets a new instance of {@link AzureAISearchVectorStoreRecordCollection} - * - * @param collectionName The name of the collection. - * @param recordClass The class type of the record. - * @param recordDefinition The record definition. - * @return The collection. - */ - @Override - public AzureAISearchVectorStoreRecordCollection getCollection( - @Nonnull String collectionName, - @Nonnull Class recordClass, - VectorStoreRecordDefinition recordDefinition) { - - if (options.getVectorStoreRecordCollectionFactory() != null) { - return options.getVectorStoreRecordCollectionFactory() - .createVectorStoreRecordCollection( - client, - collectionName, - AzureAISearchVectorStoreRecordCollectionOptions.builder() - .withRecordClass(recordClass) - .withRecordDefinition(recordDefinition) - .build()); - } - - return new AzureAISearchVectorStoreRecordCollection<>(client, collectionName, - AzureAISearchVectorStoreRecordCollectionOptions.builder() - .withRecordClass(recordClass) - .withRecordDefinition(recordDefinition) - .build()); - } - - /** - * Gets the names of all collections in the Azure AI Search vector store. - * - * @return A list of collection names. - */ - @Override - public Mono> getCollectionNamesAsync() { - return client.listIndexes().map(SearchIndex::getName).collectList(); - } - - /** - * Creates a new {@link Builder} instance. - * - * @return The new builder instance. - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Builder for {@link AzureAISearchVectorStore}. - */ - public static class Builder { - @Nullable - private SearchIndexAsyncClient client; - @Nullable - private AzureAISearchVectorStoreOptions options; - - /** - * Sets the Azure AI Search client. - * - * @param client The Azure AI Search client. - * @return The updated builder instance. - */ - @SuppressFBWarnings("EI_EXPOSE_REP2") - public Builder withClient(@Nonnull SearchIndexAsyncClient client) { - this.client = client; - return this; - } - - /** - * Sets the options for the Azure AI Search vector store. - * - * @param options The options for the Azure AI Search vector store. - * @return The updated builder instance. - */ - public Builder withOptions(@Nonnull AzureAISearchVectorStoreOptions options) { - this.options = options; - return this; - } - - /** - * Builds the Azure AI Search vector store. - * - * @return The Azure AI Search vector store. - */ - public AzureAISearchVectorStore build() { - if (client == null) { - throw new IllegalStateException("client is required"); - } - if (options == null) { - throw new IllegalStateException("options is required"); - } - - return new AzureAISearchVectorStore(client, options); - } - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollection.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollection.java deleted file mode 100644 index 5155299d8..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/azureaisearch/AzureAISearchVectorStoreRecordCollection.java +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.azureaisearch; - -import com.azure.search.documents.SearchAsyncClient; -import com.azure.search.documents.SearchDocument; -import com.azure.search.documents.indexes.SearchIndexAsyncClient; -import com.azure.search.documents.indexes.models.SearchField; -import com.azure.search.documents.indexes.models.SearchIndex; -import com.azure.search.documents.indexes.models.VectorSearch; -import com.azure.search.documents.indexes.models.VectorSearchAlgorithmConfiguration; -import com.azure.search.documents.indexes.models.VectorSearchProfile; -import com.azure.search.documents.models.IndexDocumentsResult; -import com.azure.search.documents.models.IndexingResult; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordKeyField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordVectorField; -import com.microsoft.semantickernel.exceptions.SKException; -import com.microsoft.semantickernel.data.VectorStoreRecordCollection; -import com.microsoft.semantickernel.data.VectorStoreRecordMapper; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDataField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.data.recordoptions.DeleteRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.UpsertRecordOptions; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import javax.annotation.Nonnull; -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -public class AzureAISearchVectorStoreRecordCollection - implements VectorStoreRecordCollection { - - private static final HashSet> supportedKeyTypes = new HashSet<>( - Collections.singletonList( - String.class)); - - private static final HashSet> supportedDataTypes = new HashSet<>( - Arrays.asList( - String.class, - Integer.class, - int.class, - Long.class, - long.class, - Float.class, - float.class, - Double.class, - double.class, - Boolean.class, - boolean.class, - OffsetDateTime.class)); - - private static final HashSet> supportedVectorTypes = new HashSet<>( - Arrays.asList( - List.class, - Collection.class)); - - private final SearchIndexAsyncClient client; - private final String collectionName; - private final Map clientsByIndex = new ConcurrentHashMap<>(); - private final AzureAISearchVectorStoreRecordCollectionOptions options; - private final VectorStoreRecordDefinition recordDefinition; - - // List of non-vector fields. Used to fetch only non-vector fields when vectors are not requested - private final List nonVectorFields = new ArrayList<>(); - - @SuppressFBWarnings("EI_EXPOSE_REP2") - public AzureAISearchVectorStoreRecordCollection( - @Nonnull SearchIndexAsyncClient client, - @Nonnull String collectionName, - @Nonnull AzureAISearchVectorStoreRecordCollectionOptions options) { - this.client = client; - this.collectionName = collectionName; - this.options = options; - - // If record definition is not provided, create one from the record class - this.recordDefinition = options.getRecordDefinition() == null - ? VectorStoreRecordDefinition.fromRecordClass(options.getRecordClass()) - : options.getRecordDefinition(); - - // Validate supported types - VectorStoreRecordDefinition.validateSupportedTypes( - Collections - .singletonList(recordDefinition.getKeyDeclaredField(this.options.getRecordClass())), - supportedKeyTypes); - VectorStoreRecordDefinition.validateSupportedTypes( - recordDefinition.getDataDeclaredFields(this.options.getRecordClass()), - supportedDataTypes); - VectorStoreRecordDefinition.validateSupportedTypes( - recordDefinition.getVectorDeclaredFields(this.options.getRecordClass()), - supportedVectorTypes); - - // Add non-vector fields to the list - nonVectorFields.add(this.recordDefinition.getKeyField().getName()); - nonVectorFields.addAll(this.recordDefinition.getDataFields().stream() - .map(VectorStoreRecordDataField::getName) - .collect(Collectors.toList())); - } - - @Override - public String getCollectionName() { - return collectionName; - } - - private Mono> getIndexesAsync() { - return client.listIndexes().map(SearchIndex::getName).collect(Collectors.toList()); - } - - @Override - public Mono collectionExistsAsync() { - return getIndexesAsync() - .map(list -> list.stream().anyMatch(name -> name.equalsIgnoreCase(collectionName))); - } - - @Override - public Mono createCollectionAsync() { - List searchFields = new ArrayList<>(); - List algorithms = new ArrayList<>(); - List profiles = new ArrayList<>(); - - for (VectorStoreRecordField field : this.recordDefinition.getAllFields()) { - if (field instanceof VectorStoreRecordKeyField) { - searchFields.add(AzureAISearchVectorStoreCollectionCreateMapping - .mapKeyField((VectorStoreRecordKeyField) field)); - } else if (field instanceof VectorStoreRecordDataField) { - searchFields.add(AzureAISearchVectorStoreCollectionCreateMapping - .mapDataField((VectorStoreRecordDataField) field)); - } else { - searchFields.add(AzureAISearchVectorStoreCollectionCreateMapping - .mapVectorField((VectorStoreRecordVectorField) field)); - AzureAISearchVectorStoreCollectionCreateMapping - .updateVectorSearchParameters(algorithms, profiles, - (VectorStoreRecordVectorField) field); - } - } - - SearchIndex newIndex = new SearchIndex(collectionName) - .setFields(searchFields) - .setVectorSearch(new VectorSearch() - .setAlgorithms(algorithms) - .setProfiles(profiles)); - - return client.createIndex(newIndex).then(); - } - - @Override - public Mono createCollectionIfNotExistsAsync() { - return collectionExistsAsync().flatMap( - exists -> { - if (!exists) { - return createCollectionAsync(); - } - return Mono.empty(); - }); - } - - @Override - public Mono deleteCollectionAsync() { - return client.deleteIndex(this.collectionName).then(); - } - - @Override - public Mono getAsync( - @Nonnull String key, GetRecordOptions options) { - SearchAsyncClient client = this.getSearchClient(this.collectionName); - - // If vectors are not requested, only fetch non-vector fields - List selectedFields = null; - if (options != null && !options.includeVectors()) { - selectedFields = Collections.unmodifiableList(nonVectorFields); - } - - VectorStoreRecordMapper mapper = this.options - .getVectorStoreRecordMapper(); - - // Use custom mapper if available - if (mapper != null && mapper.getStorageModelToRecordMapper() != null) { - return client.getDocument(key, SearchDocument.class) - .map(this.options.getVectorStoreRecordMapper()::mapStorageModeltoRecord); - } - - return client.getDocumentWithResponse(key, this.options.getRecordClass(), selectedFields) - .map(response -> { - if (response.getStatusCode() == 404) { - throw new SKException("Record not found: " + key); - } - return response.getValue(); - }); - - } - - @Override - public Mono> getBatchAsync( - @Nonnull List keys, - GetRecordOptions options) { - return Flux.fromIterable(keys) - .flatMap(key -> getAsync(key, options).flux()) - .collect(Collectors.toList()); - } - - @Override - public Mono upsertAsync(@Nonnull Record record, UpsertRecordOptions options) { - return upsertBatchAsync(Collections.singletonList(record), options) - .map(Collection::iterator) - .map(Iterator::next); - } - - @Override - public Mono> upsertBatchAsync( - @Nonnull List records, UpsertRecordOptions options) { - if (records.isEmpty()) { - return Mono.just(Collections.emptyList()); - } - SearchAsyncClient client = this.getSearchClient(this.collectionName); - - VectorStoreRecordMapper mapper = this.options - .getVectorStoreRecordMapper(); - Iterable documents; - - // Use custom mapper if available - if (mapper != null && mapper.getRecordToStorageModelMapper() != null) { - documents = records.stream() - .map(this.options.getVectorStoreRecordMapper()::mapRecordToStorageModel) - .collect(Collectors.toList()); - } else { - documents = records; - } - - return client.uploadDocuments(documents) - .map(IndexDocumentsResult::getResults) - .map( - results -> results.stream() - .map(IndexingResult::getKey) - .collect(Collectors.toList())); - } - - @Override - public Mono deleteAsync(String key, DeleteRecordOptions options) { - return deleteBatchAsync(Collections.singletonList(key), options); - } - - @Override - public Mono deleteBatchAsync(List keys, DeleteRecordOptions options) { - SearchAsyncClient client = this.getSearchClient(this.collectionName); - - return client.deleteDocuments(keys.stream().map(key -> { - SearchDocument document = new SearchDocument(); - document.put(this.recordDefinition.getKeyField().getName(), key); - return document; - }).collect(Collectors.toList())).then(); - } - - /** - * Get a search client for the index specified. Note: the index might not exist, but we avoid - * checking everytime and the extra latency. - * - * @param indexName Index name - * @return Search client ready to read/write - */ - protected SearchAsyncClient getSearchClient(@Nonnull String indexName) { - return clientsByIndex.computeIfAbsent( - indexName, client::getSearchAsyncClient); - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreDefaultQueryProvider.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreDefaultQueryProvider.java deleted file mode 100644 index 096f240ab..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreDefaultQueryProvider.java +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; - -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.exceptions.SKException; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordField; -import com.microsoft.semantickernel.data.recordoptions.DeleteRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.UpsertRecordOptions; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -import javax.annotation.Nonnull; -import javax.sql.DataSource; -import java.lang.reflect.Field; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class JDBCVectorStoreDefaultQueryProvider - implements JDBCVectorStoreQueryProvider { - private static final Map, String> supportedKeyTypes; - private static final Map, String> supportedDataTypes; - private static final Map, String> supportedVectorTypes; - - static { - supportedKeyTypes = new HashMap<>(); - supportedKeyTypes.put(String.class, "VARCHAR(255)"); - - supportedDataTypes = new HashMap<>(); - supportedDataTypes.put(String.class, "TEXT"); - supportedDataTypes.put(Integer.class, "INTEGER"); - supportedDataTypes.put(int.class, "INTEGER"); - supportedDataTypes.put(Long.class, "BIGINT"); - supportedDataTypes.put(long.class, "BIGINT"); - supportedDataTypes.put(Float.class, "REAL"); - supportedDataTypes.put(float.class, "REAL"); - supportedDataTypes.put(Double.class, "DOUBLE"); - supportedDataTypes.put(double.class, "DOUBLE"); - supportedDataTypes.put(Boolean.class, "BOOLEAN"); - supportedDataTypes.put(boolean.class, "BOOLEAN"); - supportedDataTypes.put(OffsetDateTime.class, "TIMESTAMPTZ"); - - supportedVectorTypes = new HashMap<>(); - supportedVectorTypes.put(String.class, "TEXT"); - supportedVectorTypes.put(List.class, "TEXT"); - supportedVectorTypes.put(Collection.class, "TEXT"); - } - private final DataSource dataSource; - private final String collectionsTable; - private final String prefixForCollectionTables; - - @SuppressFBWarnings("EI_EXPOSE_REP2") // DataSource is not exposed - protected JDBCVectorStoreDefaultQueryProvider( - @Nonnull DataSource dataSource, - @Nonnull String collectionsTable, - @Nonnull String prefixForCollectionTables) { - this.dataSource = dataSource; - this.collectionsTable = collectionsTable; - this.prefixForCollectionTables = prefixForCollectionTables; - } - - /** - * Creates a new builder. - * @return the builder - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Formats a wildcard string for a query. - * @param wildcards the number of wildcards - * @return the formatted wildcard string - */ - protected String getWildcardString(int wildcards) { - StringBuilder wildcardString = new StringBuilder(); - for (int i = 0; i < wildcards; ++i) { - wildcardString.append("?"); - if (i < wildcards - 1) { - wildcardString.append(", "); - } - } - return wildcardString.toString(); - } - - /** - * Formats the query columns from a record definition. - * @param fields the fields to get the columns from - * @return the formatted query columns - */ - protected String getQueryColumnsFromFields(List fields) { - return fields.stream().map(VectorStoreRecordField::getName) - .collect(Collectors.joining(", ")); - } - - protected String getColumnNamesAndTypes(List fields, Map, String> types) { - List columns = fields.stream() - .map(field -> field.getName() + " " + types.get(field.getType())) - .collect(Collectors.toList()); - - return String.join(", ", columns); - } - - protected String getCollectionTableName(String collectionName) { - return validateSQLidentifier(prefixForCollectionTables + collectionName); - } - - /** - * Prepares the vector store. - * Executes any necessary setup steps for the vector store. - * - * @throws SKException if an error occurs while preparing the vector store - */ - @Override - public void prepareVectorStore() { - String createCollectionsTable = "CREATE TABLE IF NOT EXISTS " - + validateSQLidentifier(collectionsTable) - + " (collectionId VARCHAR(255) PRIMARY KEY);"; - - try (Connection connection = dataSource.getConnection(); - PreparedStatement createTable = connection.prepareStatement(createCollectionsTable)) { - createTable.execute(); - } catch (SQLException e) { - throw new SKException("Failed to prepare vector store", e); - } - } - - /** - * Checks if the types of the record class fields are supported. - * - * @param recordClass the record class - * @param recordDefinition the record definition - * @throws IllegalArgumentException if the types are not supported - */ - @Override - public void validateSupportedTypes(Class recordClass, - VectorStoreRecordDefinition recordDefinition) { - VectorStoreRecordDefinition.validateSupportedTypes( - Collections.singletonList(recordDefinition.getKeyDeclaredField(recordClass)), - supportedKeyTypes.keySet()); - VectorStoreRecordDefinition.validateSupportedTypes( - recordDefinition.getDataDeclaredFields(recordClass), supportedDataTypes.keySet()); - VectorStoreRecordDefinition.validateSupportedTypes( - recordDefinition.getVectorDeclaredFields(recordClass), supportedVectorTypes.keySet()); - } - - /** - * Checks if a collection exists. - * - * @param collectionName the collection name - * @return true if the collection exists, false otherwise - * @throws SKException if an error occurs while checking if the collection exists - */ - @Override - public boolean collectionExists(String collectionName) { - String query = "SELECT 1 FROM " + validateSQLidentifier(collectionsTable) - + " WHERE collectionId = ?"; - - try (Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement(query)) { - statement.setObject(1, collectionName); - - return statement.executeQuery().next(); - } catch (SQLException e) { - throw new SKException("Failed to check if collection exists", e); - } - } - - /** - * Creates a collection. - * - * @param collectionName the collection name - * @param recordClass the record class - * @param recordDefinition the record definition - * @throws SKException if an error occurs while creating the collection - */ - @Override - @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") // SQL query is generated dynamically with valid identifiers - public void createCollection(String collectionName, Class recordClass, - VectorStoreRecordDefinition recordDefinition) { - Field keyDeclaredField = recordDefinition.getKeyDeclaredField(recordClass); - List dataDeclaredFields = recordDefinition.getDataDeclaredFields(recordClass); - List vectorDeclaredFields = recordDefinition.getVectorDeclaredFields(recordClass); - - String createStorageTable = "CREATE TABLE IF NOT EXISTS " - + getCollectionTableName(collectionName) - + " (" + keyDeclaredField.getName() + " VARCHAR(255) PRIMARY KEY, " - + getColumnNamesAndTypes(dataDeclaredFields, supportedDataTypes) + ", " - + getColumnNamesAndTypes(vectorDeclaredFields, supportedVectorTypes) + ");"; - - String insertCollectionQuery = "INSERT INTO " + validateSQLidentifier(collectionsTable) - + " (collectionId) VALUES (?)"; - - try (Connection connection = dataSource.getConnection(); - PreparedStatement createTable = connection.prepareStatement(createStorageTable)) { - createTable.execute(); - } catch (SQLException e) { - throw new SKException("Failed to create collection", e); - } - - try (Connection connection = dataSource.getConnection(); - PreparedStatement insert = connection.prepareStatement(insertCollectionQuery)) { - insert.setObject(1, collectionName); - insert.execute(); - } catch (SQLException e) { - throw new SKException("Failed to insert collection", e); - } - } - - /** - * Deletes a collection. - * - * @param collectionName the collection name - * @throws SKException if an error occurs while deleting the collection - */ - @Override - public void deleteCollection(String collectionName) { - String deleteCollectionOperation = "DELETE FROM " + validateSQLidentifier(collectionsTable) - + " WHERE collectionId = ?"; - String dropTableOperation = "DROP TABLE " + getCollectionTableName(collectionName); - - try (Connection connection = dataSource.getConnection(); - PreparedStatement deleteCollection = connection - .prepareStatement(deleteCollectionOperation)) { - deleteCollection.setObject(1, collectionName); - deleteCollection.execute(); - } catch (SQLException e) { - throw new SKException("Failed to delete collection", e); - } - - try (Connection connection = dataSource.getConnection(); - PreparedStatement dropTable = connection.prepareStatement(dropTableOperation)) { - dropTable.execute(); - } catch (SQLException e) { - throw new SKException("Failed to drop table", e); - } - } - - /** - * Gets the collection names. - * - * @return the collection names - * @throws SKException if an error occurs while getting the collection names - */ - @Override - public List getCollectionNames() { - String query = "SELECT collectionId FROM " + validateSQLidentifier(collectionsTable); - - try (Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement(query)) { - List collectionNames = new ArrayList<>(); - ResultSet resultSet = statement.executeQuery(); - - while (resultSet.next()) { - collectionNames.add(resultSet.getString(1)); - } - - return Collections.unmodifiableList(collectionNames); - } catch (SQLException e) { - throw new SKException("Failed to get collection names", e); - } - } - - /** - * Gets a list of records from the store. - * - * @param collectionName the collection name - * @param keys the keys - * @param recordDefinition the record definition - * @param mapper the mapper - * @param options the options - * @return the records - * @param the record type - * @throws SKException if an error occurs while getting the records - */ - @Override - public List getRecords(String collectionName, List keys, - VectorStoreRecordDefinition recordDefinition, JDBCVectorStoreRecordMapper mapper, - GetRecordOptions options) { - List fields; - if (options == null || options.includeVectors()) { - fields = recordDefinition.getAllFields(); - } else { - fields = recordDefinition.getNonVectorFields(); - } - - String query = "SELECT " + getQueryColumnsFromFields(fields) - + " FROM " + getCollectionTableName(collectionName) - + " WHERE " + recordDefinition.getKeyField().getName() - + " IN (" + getWildcardString(keys.size()) + ")"; - - try (Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement(query)) { - for (int i = 0; i < keys.size(); ++i) { - statement.setObject(i + 1, keys.get(i)); - } - - List records = new ArrayList<>(); - ResultSet resultSet = statement.executeQuery(); - - while (resultSet.next()) { - records.add(mapper.mapStorageModeltoRecord(resultSet)); - } - - return Collections.unmodifiableList(records); - } catch (SQLException e) { - throw new SKException("Failed to set statement values", e); - } - } - - @Override - public void upsertRecords(String collectionName, List records, - VectorStoreRecordDefinition recordDefinition, UpsertRecordOptions options) { - throw new UnsupportedOperationException( - "Upsert is not supported. Try with a specific query provider."); - } - - /** - * Deletes records. - * - * @param collectionName the collection name - * @param keys the keys - * @param recordDefinition the record definition - * @param options the options - * @throws SKException if an error occurs while deleting the records - */ - @Override - public void deleteRecords(String collectionName, List keys, - VectorStoreRecordDefinition recordDefinition, DeleteRecordOptions options) { - String query = "DELETE FROM " + getCollectionTableName(collectionName) - + " WHERE " + recordDefinition.getKeyField().getName() - + " IN (" + getWildcardString(keys.size()) + ")"; - - try (Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement(query)) { - for (int i = 0; i < keys.size(); ++i) { - statement.setObject(i + 1, keys.get(i)); - } - - statement.execute(); - } catch (SQLException e) { - throw new SKException("Failed to set statement values", e); - } - } - - /** - * Validates an SQL identifier. - * - * @param identifier the identifier - * @return the identifier if it is valid - * @throws IllegalArgumentException if the identifier is invalid - */ - public static String validateSQLidentifier(String identifier) { - if (identifier.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { - return identifier; - } - throw new IllegalArgumentException("Invalid SQL identifier: " + identifier); - } - - /** - * The builder for {@link JDBCVectorStoreDefaultQueryProvider}. - */ - public static class Builder - implements JDBCVectorStoreQueryProvider.Builder { - private DataSource dataSource; - private String collectionsTable = DEFAULT_COLLECTIONS_TABLE; - private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; - - /** - * Sets the data source. - * @param dataSource the data source - * @return the builder - */ - @SuppressFBWarnings("EI_EXPOSE_REP2") // DataSource is not exposed - public Builder withDataSource(DataSource dataSource) { - this.dataSource = dataSource; - return this; - } - - /** - * Sets the collections table name. - * @param collectionsTable the collections table name - * @return the builder - */ - public Builder withCollectionsTable(String collectionsTable) { - this.collectionsTable = validateSQLidentifier(collectionsTable); - return this; - } - - /** - * Sets the prefix for collection tables. - * @param prefixForCollectionTables the prefix for collection tables - * @return the builder - */ - public Builder withPrefixForCollectionTables(String prefixForCollectionTables) { - this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); - return this; - } - - @Override - public JDBCVectorStoreDefaultQueryProvider build() { - if (dataSource == null) { - throw new IllegalArgumentException("DataSource is required"); - } - - return new JDBCVectorStoreDefaultQueryProvider(dataSource, collectionsTable, - prefixForCollectionTables); - } - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreQueryProvider.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreQueryProvider.java deleted file mode 100644 index 26d976aa5..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreQueryProvider.java +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; - -import com.microsoft.semantickernel.builders.SemanticKernelBuilder; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.data.recordoptions.DeleteRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions; -import com.microsoft.semantickernel.data.recordoptions.UpsertRecordOptions; - -import java.util.List; - -/** - * The JDBC vector store query provider. - * Provides the necessary methods to interact with a JDBC vector store and vector store collections. - */ -public interface JDBCVectorStoreQueryProvider { - /** - * The default name for the collections table. - */ - String DEFAULT_COLLECTIONS_TABLE = "SKCollections"; - - /** - * The prefix for collection tables. - */ - String DEFAULT_PREFIX_FOR_COLLECTION_TABLES = "SKCollection_"; - - /** - * Prepares the vector store. - * Executes any necessary setup steps for the vector store. - */ - void prepareVectorStore(); - - /** - * Checks if the types of the record class fields are supported. - * - * @param recordClass the record class - * @param recordDefinition the record definition - */ - void validateSupportedTypes(Class recordClass, VectorStoreRecordDefinition recordDefinition); - - /** - * Checks if a collection exists. - * - * @param collectionName the collection name - * @return true if the collection exists, false otherwise - */ - boolean collectionExists(String collectionName); - - /** - * Creates a collection. - * - * @param collectionName the collection name - * @param recordClass the record class - * @param recordDefinition the record definition - */ - void createCollection(String collectionName, Class recordClass, - VectorStoreRecordDefinition recordDefinition); - - /** - * Deletes a collection. - * - * @param collectionName the collection name - */ - void deleteCollection(String collectionName); - - /** - * Gets the collection names. - * - * @return the collection names - */ - List getCollectionNames(); - - /** - * Gets records. - * - * @param collectionName the collection name - * @param keys the keys - * @param recordDefinition the record definition - * @param mapper the mapper - * @param options the options - * @return the records - */ - List getRecords(String collectionName, List keys, - VectorStoreRecordDefinition recordDefinition, JDBCVectorStoreRecordMapper mapper, - GetRecordOptions options); - - /** - * Upserts records. - * - * @param collectionName the collection name - * @param records the records - * @param vectorStoreRecordDefinition the record definition - * @param options the options - */ - void upsertRecords(String collectionName, List records, - VectorStoreRecordDefinition vectorStoreRecordDefinition, UpsertRecordOptions options); - - /** - * Deletes records. - * - * @param collectionName the collection name - * @param keys the keys - * @param recordDefinition the record definition - * @param options the options - */ - void deleteRecords(String collectionName, List keys, - VectorStoreRecordDefinition recordDefinition, DeleteRecordOptions options); - - /** - * The builder for the JDBC vector store query provider. - */ - interface Builder extends SemanticKernelBuilder { - - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollectionFactory.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollectionFactory.java deleted file mode 100644 index 70b62a7eb..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordCollectionFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; - -import javax.sql.DataSource; -import java.sql.Connection; - -/** - * Factory for creating JDBC vector store record collections. - */ -public interface JDBCVectorStoreRecordCollectionFactory { - /** - * Creates a new JDBC vector store record collection. - * - * @param options The options for the collection. - * @return The new JDBC vector store record collection. - */ - JDBCVectorStoreRecordCollection createVectorStoreRecordCollection( - DataSource dataSource, - String collectionName, - JDBCVectorStoreRecordCollectionOptions options); -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordMapper.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordMapper.java deleted file mode 100644 index 6eff0c7d1..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/JDBCVectorStoreRecordMapper.java +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.semantickernel.builders.SemanticKernelBuilder; -import com.microsoft.semantickernel.exceptions.SKException; -import com.microsoft.semantickernel.data.VectorStoreRecordMapper; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordVectorField; - -import java.sql.ResultSetMetaData; -import java.util.List; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.function.Function; - -public class JDBCVectorStoreRecordMapper - extends VectorStoreRecordMapper { - - /** - * Constructs a new instance of the VectorStoreRecordMapper. - * - * @param storageModelToRecordMapper the function to convert a storage model to a record - */ - protected JDBCVectorStoreRecordMapper(Function storageModelToRecordMapper) { - super(null, storageModelToRecordMapper); - } - - /** - * Creates a new builder. - * - * @param the record type - * @return the builder - */ - public static Builder builder() { - return new Builder<>(); - } - - /** - * Operation not supported. - */ - @Override - public ResultSet mapRecordToStorageModel(Record record) { - throw new UnsupportedOperationException("Not implemented"); - } - - public static class Builder - implements SemanticKernelBuilder> { - private Class recordClass; - private VectorStoreRecordDefinition vectorStoreRecordDefinition; - - /** - * Sets the record class. - * - * @param recordClass the record class - * @return the builder - */ - public Builder withRecordClass(Class recordClass) { - this.recordClass = recordClass; - return this; - } - - /** - * Sets the vector store record definition. - * - * @param vectorStoreRecordDefinition the vector store record definition - * @return the builder - */ - public Builder withVectorStoreRecordDefinition( - VectorStoreRecordDefinition vectorStoreRecordDefinition) { - this.vectorStoreRecordDefinition = vectorStoreRecordDefinition; - return this; - } - - /** - * Builds the {@link JDBCVectorStoreRecordMapper}. - * - * @return the {@link JDBCVectorStoreRecordMapper} - */ - public JDBCVectorStoreRecordMapper build() { - if (recordClass == null) { - throw new IllegalArgumentException("recordClass is required"); - } - if (vectorStoreRecordDefinition == null) { - throw new IllegalArgumentException("vectorStoreRecordDefinition is required"); - } - - return new JDBCVectorStoreRecordMapper<>( - resultSet -> { - try { - Constructor constructor = recordClass.getDeclaredConstructor(); - constructor.setAccessible(true); - Record record = (Record) constructor.newInstance(); - - // Select fields from the record definition. - // Check if vector fields are present in the result set. - List fields; - ResultSetMetaData metaData = resultSet.getMetaData(); - if (metaData.getColumnCount() == vectorStoreRecordDefinition.getAllFields() - .size()) { - fields = vectorStoreRecordDefinition.getAllFields(); - } else { - fields = vectorStoreRecordDefinition.getNonVectorFields(); - } - - for (VectorStoreRecordField field : fields) { - Object value = resultSet.getObject(field.getName()); - Field recordField = recordClass.getDeclaredField(field.getName()); - recordField.setAccessible(true); - - // If the field is a vector field, deserialize the JSON string - if (field instanceof VectorStoreRecordVectorField) { - Class vectorType = recordField.getType(); - - // If the vector type is a string, set the value directly - if (vectorType.equals(String.class)) { - recordField.set(record, value); - } else { - // Deserialize the JSON string to the vector type - recordField.set(record, - new ObjectMapper().readValue((String) value, vectorType)); - } - } else { - recordField.set(record, value); - } - } - - return record; - } catch (NoSuchMethodException e) { - throw new SKException("Default constructor not found.", e); - } catch (InstantiationException | InvocationTargetException e) { - throw new SKException(String.format( - "SK cannot instantiate %s. A custom mapper is required.", - recordClass.getName()), e); - } catch (JsonProcessingException e) { - throw new SKException(String.format( - "SK cannot deserialize %s. A custom mapper is required.", - recordClass.getName()), e); - } catch (SQLException | NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - }); - } - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/MySQLVectorStoreQueryProvider.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/MySQLVectorStoreQueryProvider.java deleted file mode 100644 index 72ecd87e1..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/MySQLVectorStoreQueryProvider.java +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordKeyField; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordVectorField; -import com.microsoft.semantickernel.data.recordoptions.UpsertRecordOptions; -import com.microsoft.semantickernel.exceptions.SKException; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -import javax.sql.DataSource; -import java.lang.reflect.Field; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.List; - -public class MySQLVectorStoreQueryProvider extends - JDBCVectorStoreDefaultQueryProvider implements JDBCVectorStoreQueryProvider { - - private final DataSource dataSource; - - @SuppressFBWarnings("EI_EXPOSE_REP2") - private MySQLVectorStoreQueryProvider(DataSource dataSource, String collectionsTable, - String prefixForCollectionTables) { - super(dataSource, collectionsTable, prefixForCollectionTables); - this.dataSource = dataSource; - } - - /** - * Creates a new builder. - * @return the builder - */ - public static Builder builder() { - return new Builder(); - } - - private void setStatementValues(PreparedStatement statement, Object record, - List fields) { - for (int i = 0; i < fields.size(); ++i) { - VectorStoreRecordField field = fields.get(i); - try { - Field recordField = record.getClass().getDeclaredField(field.getName()); - recordField.setAccessible(true); - Object value = recordField.get(record); - - if (field instanceof VectorStoreRecordKeyField) { - statement.setObject(i + 1, (String) value); - } else if (field instanceof VectorStoreRecordVectorField) { - Class vectorType = record.getClass().getDeclaredField(field.getName()) - .getType(); - - // If the vector field is other than String, serialize it to JSON - if (vectorType.equals(String.class)) { - statement.setObject(i + 1, value); - } else { - // Serialize the vector to JSON - statement.setObject(i + 1, new ObjectMapper().writeValueAsString(value)); - } - } else { - statement.setObject(i + 1, value); - } - } catch (NoSuchFieldException | IllegalAccessException | SQLException e) { - throw new SKException("Failed to set statement values", e); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - } - - /** - * Upserts records into the collection. - * @param collectionName the collection name - * @param records the records to upsert - * @param recordDefinition the record definition - * @param options the upsert options - * @throws SKException if the upsert fails - */ - @Override - @SuppressFBWarnings("SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING") // SQL query is generated dynamically with valid identifiers - public void upsertRecords(String collectionName, List records, - VectorStoreRecordDefinition recordDefinition, UpsertRecordOptions options) { - validateSQLidentifier(getCollectionTableName(collectionName)); - - List fields = recordDefinition.getAllFields(); - - StringBuilder onDuplicateKeyUpdate = new StringBuilder(); - for (int i = 0; i < fields.size(); ++i) { - VectorStoreRecordField field = fields.get(i); - if (i > 0) { - onDuplicateKeyUpdate.append(", "); - } - - onDuplicateKeyUpdate.append(field.getName()).append(" = VALUES(") - .append(field.getName()).append(")"); - } - - String query = "INSERT INTO " + getCollectionTableName(collectionName) - + " (" + getQueryColumnsFromFields(fields) + ")" - + " VALUES (" + getWildcardString(fields.size()) + ")" - + " ON DUPLICATE KEY UPDATE " + onDuplicateKeyUpdate; - - try (Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement(query)) { - for (Object record : records) { - setStatementValues(statement, record, recordDefinition.getAllFields()); - statement.addBatch(); - } - - statement.executeBatch(); - } catch (SQLException e) { - throw new SKException("Failed to upsert records", e); - } - } - - public static class Builder - extends JDBCVectorStoreDefaultQueryProvider.Builder { - private DataSource dataSource; - private String collectionsTable = DEFAULT_COLLECTIONS_TABLE; - private String prefixForCollectionTables = DEFAULT_PREFIX_FOR_COLLECTION_TABLES; - - @SuppressFBWarnings("EI_EXPOSE_REP2") - public Builder withDataSource(DataSource dataSource) { - this.dataSource = dataSource; - return this; - } - - /** - * Sets the collections table name. - * @param collectionsTable the collections table name - * @return the builder - */ - public Builder withCollectionsTable(String collectionsTable) { - this.collectionsTable = validateSQLidentifier(collectionsTable); - return this; - } - - /** - * Sets the prefix for collection tables. - * @param prefixForCollectionTables the prefix for collection tables - * @return the builder - */ - public Builder withPrefixForCollectionTables(String prefixForCollectionTables) { - this.prefixForCollectionTables = validateSQLidentifier(prefixForCollectionTables); - return this; - } - - public MySQLVectorStoreQueryProvider build() { - if (dataSource == null) { - throw new SKException("DataSource is required"); - } - - return new MySQLVectorStoreQueryProvider(dataSource, collectionsTable, - prefixForCollectionTables); - } - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/SQLVectorStore.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/SQLVectorStore.java deleted file mode 100644 index 10e4d2eff..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/jdbc/SQLVectorStore.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.jdbc; - -import com.microsoft.semantickernel.data.VectorStore; -import com.microsoft.semantickernel.data.VectorStoreRecordCollection; -import reactor.core.publisher.Mono; - -public interface SQLVectorStore> - extends VectorStore { - - /** - * Prepares the vector store. - * - * @return A {@link Mono} that completes when the vector store is prepared to be used. - */ - Mono prepareAsync(); -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollectionFactory.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollectionFactory.java deleted file mode 100644 index 45417980d..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordCollectionFactory.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.redis; - -import redis.clients.jedis.JedisPooled; - -/** - * Factory for creating Redis vector store record collections. - * - */ -public interface RedisVectorStoreRecordCollectionFactory { - /** - * Creates a new vector store record collection. - * - * @param client The Redis client. - * @param collectionName The name of the collection. - * @param options The options for the collection. - * @return The collection. - */ - RedisVectorStoreRecordCollection createVectorStoreRecordCollection( - JedisPooled client, - String collectionName, - RedisVectorStoreRecordCollectionOptions options); -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordMapper.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordMapper.java deleted file mode 100644 index a4f5f7982..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/connectors/data/redis/RedisVectorStoreRecordMapper.java +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.connectors.data.redis; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.microsoft.semantickernel.builders.SemanticKernelBuilder; -import com.microsoft.semantickernel.data.VectorStoreRecordMapper; -import com.microsoft.semantickernel.exceptions.SKException; - -import javax.annotation.Nullable; -import java.util.AbstractMap; -import java.util.Map.Entry; -import java.util.function.Function; - -public class RedisVectorStoreRecordMapper - extends VectorStoreRecordMapper> { - - private RedisVectorStoreRecordMapper( - Function> toStorageModelMapper, - Function, Record> toRecordMapper) { - super(toStorageModelMapper, toRecordMapper); - } - - /** - * Creates a new builder. - * - * @param the record type - * @return the builder - */ - public static Builder builder() { - return new Builder<>(); - } - - /** - * Creates a new builder. - * - * @param the record type - */ - public static class Builder - implements SemanticKernelBuilder> { - @Nullable - private String keyFieldName; - @Nullable - private Class recordClass; - - /** - * Sets the key field name in the record. - * - * @param keyFieldName the key field - * @return the builder - */ - public Builder withKeyFieldName(String keyFieldName) { - this.keyFieldName = keyFieldName; - return this; - } - - /** - * Sets the record class. - * - * @param recordClass the record class - * @return the builder - */ - public Builder withRecordClass(Class recordClass) { - this.recordClass = recordClass; - return this; - } - - /** - * Builds the {@link RedisVectorStoreRecordMapper}. - * - * @return the {@link RedisVectorStoreRecordMapper} - */ - @Override - public RedisVectorStoreRecordMapper build() { - if (keyFieldName == null) { - throw new IllegalArgumentException("keyFieldName is required"); - } - if (recordClass == null) { - throw new IllegalArgumentException("recordClass is required"); - } - ObjectMapper mapper = new ObjectMapper(); - - return new RedisVectorStoreRecordMapper<>(record -> { - try { - ObjectNode jsonNode = mapper.valueToTree(record); - String key = jsonNode.get(keyFieldName).asText(); - jsonNode.remove(keyFieldName); - - return new AbstractMap.SimpleEntry<>(key, jsonNode); - } catch (Exception e) { - throw new SKException( - "Failure to serialize object, by default the Redis connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", - e); - } - }, storageModel -> { - try { - ObjectNode jsonNode = mapper.valueToTree(storageModel.getValue()); - // Add the key back to the record - jsonNode.put(keyFieldName, storageModel.getKey()); - return mapper.convertValue(jsonNode, recordClass); - } catch (Exception e) { - throw new SKException( - "Failure to deserialize object, by default the Redis connector uses Jackson, ensure your model object can be serialized by Jackson, i.e the class is visible, has getters, constructor, annotations etc.", - e); - } - }); - } - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStore.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStore.java deleted file mode 100644 index d778829af..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VectorStore.java +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data; - -import com.microsoft.semantickernel.data.VectorStoreRecordCollection; -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import reactor.core.publisher.Mono; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.List; - -/** - * Represents a vector store. - * - * @param The type of the record collection. - */ -public interface VectorStore> { - - /** - * Gets a collection from the vector store. - * - * @param collectionName The name of the collection. - * @param recordClass The class type of the record. - * @param recordDefinition The record definition. - * @return The collection. - */ - RecordCollection getCollection( - @Nonnull String collectionName, - @Nonnull Class recordClass, - @Nullable VectorStoreRecordDefinition recordDefinition); - - /** - * Gets the names of all collections in the vector store. - * - * @return A list of collection names. - */ - Mono> getCollectionNamesAsync(); -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStore.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStore.java deleted file mode 100644 index ef0742473..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/VolatileVectorStore.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data; - -import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition; -import reactor.core.publisher.Mono; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class VolatileVectorStore implements VectorStore> { - private final Map> collections; - - public VolatileVectorStore() { - this.collections = new ConcurrentHashMap<>(); - } - - /** - * Gets a collection from the vector store. - * - * @param collectionName The name of the collection. - * @param recordDefinition The record definition. - * @return The collection. - */ - @Override - public VolatileVectorStoreRecordCollection getCollection( - @Nonnull String collectionName, @Nonnull Class recordClass, - VectorStoreRecordDefinition recordDefinition) { - return new VolatileVectorStoreRecordCollection<>( - collectionName, - collections, - VolatileVectorStoreRecordCollectionOptions.builder() - .withRecordClass(recordClass) - .withRecordDefinition(recordDefinition) - .build()); - } - - /** - * Gets the names of all collections in the vector store. - * - * @return A list of collection names. - */ - @Override - public Mono> getCollectionNamesAsync() { - return Mono.just(new ArrayList<>(collections.keySet())); - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordDataAttribute.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordDataAttribute.java deleted file mode 100644 index cc4d233df..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordDataAttribute.java +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recordattributes; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Represents a data attribute in a record. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface VectorStoreRecordDataAttribute { - /** - * Storage name of the field. - */ - String storageName() default ""; - - /** - * Whether the field has a vector representation. - */ - boolean hasEmbedding() default false; - - /** - * Name of the field that contains the vector representation. - */ - String embeddingFieldName() default ""; - - /** - * Whether the field is filterable. - */ - boolean isFilterable() default false; -} \ No newline at end of file diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordVectorAttribute.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordVectorAttribute.java deleted file mode 100644 index 3f404652e..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordattributes/VectorStoreRecordVectorAttribute.java +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recordattributes; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Represents a vector attribute in a record. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface VectorStoreRecordVectorAttribute { - - /** - * Number of dimensions in the vector. - */ - int dimensions(); - - /** - * Storage name of the field. - */ - String storageName() default ""; - - /** - * Type of index to be used for the vector. - */ - String indexKind() default ""; - - /** - * Distance function to be used for to compute the distance between vectors. - */ - String distanceFunction() default ""; -} \ No newline at end of file diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/DistanceFunction.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/DistanceFunction.java deleted file mode 100644 index b41aa32e0..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/DistanceFunction.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recorddefinition; - -public enum DistanceFunction { - COSINE_SIMILARITY("cosineSimilarity"), DOT_PRODUCT("dotProduct"), EUCLIDEAN("euclidean"); - - private final String value; - - DistanceFunction(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - /** - * Converts a string to a DistanceFunction. - * If the string is null or empty, the method returns DistanceFunction.COSINE_SIMILARITY. - * - * @param text the string to convert - * @return the DistanceFunction - */ - public static DistanceFunction fromString(String text) { - if (text == null || text.isEmpty()) { - return COSINE_SIMILARITY; - } - - for (DistanceFunction b : DistanceFunction.values()) { - if (b.value.equalsIgnoreCase(text)) { - return b; - } - } - throw new IllegalArgumentException("No distance function with value " + text + " found"); - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/IndexKind.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/IndexKind.java deleted file mode 100644 index 62832de27..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/IndexKind.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recorddefinition; - -public enum IndexKind { - HNSW("Hnsw"), FLAT("Flat"); - - private final String value; - - IndexKind(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - /** - * Converts a string to an IndexKind. - * If the string is null or empty, the method returns IndexKind.FLAT. - * - * @param text the string to convert - * @return the IndexKind - */ - public static IndexKind fromString(String text) { - if (text == null || text.isEmpty()) { - return FLAT; - } - - for (IndexKind b : IndexKind.values()) { - if (b.value.equalsIgnoreCase(text)) { - return b; - } - } - throw new IllegalArgumentException("No index kind with value " + text + " found"); - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordDataField.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordDataField.java deleted file mode 100644 index af43821a6..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordDataField.java +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recorddefinition; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class VectorStoreRecordDataField extends VectorStoreRecordField { - private final boolean hasEmbedding; - @Nullable - private final String embeddingFieldName; - @Nullable - private final Class fieldType; - private final boolean isFilterable; - - public static Builder builder() { - return new Builder(); - } - - /** - * Creates a new instance of the VectorStoreRecordDataField class. - * - * @param name the name of the field - * @param storageName the storage name of the field - * @param hasEmbedding a value indicating whether the field has an embedding - * @param embeddingFieldName the name of the embedding - * @param fieldType the field type - * @param isFilterable a value indicating whether the field is filterable - */ - public VectorStoreRecordDataField( - @Nonnull String name, - @Nullable String storageName, - boolean hasEmbedding, - @Nullable String embeddingFieldName, - @Nullable Class fieldType, - boolean isFilterable) { - super(name, storageName); - this.hasEmbedding = hasEmbedding; - this.embeddingFieldName = embeddingFieldName; - this.fieldType = fieldType; - this.isFilterable = isFilterable; - } - - /** - * Gets a value indicating whether the field has an embedding. - * - * @return a value indicating whether the field has an embedding - */ - public boolean hasEmbedding() { - return hasEmbedding; - } - - /** - * Gets the name of the embedding. - * - * @return the name of the embedding - */ - @Nullable - public String getEmbeddingFieldName() { - return embeddingFieldName; - } - - /** - * Gets the field type. - * - * @return the field type - */ - @Nullable - public Class getFieldType() { - return fieldType; - } - - /** - * Gets a value indicating whether the field is filterable. - * - * @return a value indicating whether the field is filterable - */ - public boolean isFilterable() { - return isFilterable; - } - - public static class Builder - extends VectorStoreRecordField.Builder { - private boolean hasEmbedding; - @Nullable - private String embeddingFieldName; - @Nullable - private Class fieldType; - private boolean isFilterable; - - /** - * Sets a value indicating whether the field has an embedding. - * - * @param hasEmbedding a value indicating whether the field has an embedding - * @return the builder - */ - public Builder withHasEmbedding(boolean hasEmbedding) { - this.hasEmbedding = hasEmbedding; - return this; - } - - /** - * Sets the name of the embedding field. - * - * @param embeddingFieldName the name of the embedding - * @return the builder - */ - public Builder withEmbeddingFieldName(String embeddingFieldName) { - this.embeddingFieldName = embeddingFieldName; - return this; - } - - /** - * Sets the field type. - * - * @param fieldType the field type - * @return the builder - */ - public Builder withFieldType(Class fieldType) { - this.fieldType = fieldType; - return this; - } - - /** - * Sets a value indicating whether the field is filterable. - * - * @param isFilterable a value indicating whether the field is filterable - * @return the builder - */ - public Builder withIsFilterable(boolean isFilterable) { - this.isFilterable = isFilterable; - return this; - } - - /** - * Builds a new instance of the VectorStoreRecordDataField class. - * - * @return a new instance of the VectorStoreRecordDataField class - */ - @Override - public VectorStoreRecordDataField build() { - if (name == null) { - throw new IllegalArgumentException("name is required"); - } - if (hasEmbedding && embeddingFieldName == null) { - throw new IllegalArgumentException( - "embeddingFieldName is required when hasEmbedding is true"); - } - - return new VectorStoreRecordDataField( - name, - storageName, - hasEmbedding, - embeddingFieldName, - fieldType, - isFilterable); - } - } - -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordDefinition.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordDefinition.java deleted file mode 100644 index 39e04a3ff..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordDefinition.java +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recorddefinition; - -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordDataAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordKeyAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordVectorAttribute; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Represents a definition of a vector store record. - */ -public class VectorStoreRecordDefinition { - private final VectorStoreRecordKeyField keyField; - private final List dataFields; - private final List vectorFields; - - public VectorStoreRecordKeyField getKeyField() { - return keyField; - } - - public List getDataFields() { - return Collections.unmodifiableList(dataFields); - } - - /** - * Gets the vector fields in the record definition. - * @return List of VectorStoreRecordVectorField - */ - public List getVectorFields() { - return Collections.unmodifiableList(vectorFields); - } - - /** - * Gets all fields in the record definition. - * @return List of VectorStoreRecordField - */ - public List getAllFields() { - List fields = new ArrayList<>(); - fields.add(keyField); - fields.addAll(dataFields); - fields.addAll(vectorFields); - return fields; - } - - public List getNonVectorFields() { - List fields = new ArrayList<>(); - fields.add(keyField); - fields.addAll(dataFields); - return fields; - } - - private enum DeclaredFieldType { - KEY, DATA, VECTOR - } - - private List getDeclaredFields(Class recordClass, List fields, - DeclaredFieldType fieldType) { - List declaredFields = new ArrayList<>(); - for (VectorStoreRecordField field : fields) { - try { - Field declaredField = recordClass.getDeclaredField(field.getName()); - declaredFields.add(declaredField); - } catch (NoSuchFieldException e) { - throw new IllegalArgumentException( - String.format("%s field not found in record class: %s", fieldType, - field.getName())); - } - } - return declaredFields; - } - - public Field getKeyDeclaredField(Class recordClass) { - try { - return recordClass.getDeclaredField(keyField.getName()); - } catch (NoSuchFieldException e) { - throw new IllegalArgumentException( - "Key field not found in record class: " + keyField.getName()); - } - } - - public List getDataDeclaredFields(Class recordClass) { - return getDeclaredFields( - recordClass, - dataFields.stream().map(f -> (VectorStoreRecordField) f).collect(Collectors.toList()), - DeclaredFieldType.DATA); - } - - public List getVectorDeclaredFields(Class recordClass) { - return getDeclaredFields( - recordClass, - vectorFields.stream().map(f -> (VectorStoreRecordField) f).collect(Collectors.toList()), - DeclaredFieldType.VECTOR); - } - - private VectorStoreRecordDefinition( - VectorStoreRecordKeyField keyField, - List dataFields, - List vectorFields) { - this.keyField = keyField; - this.dataFields = dataFields; - this.vectorFields = vectorFields; - } - - private static VectorStoreRecordDefinition checkFields( - List keyFields, - List dataFields, - List vectorFields) { - if (keyFields.size() != 1) { - throw new IllegalArgumentException("Exactly one key field is required"); - } - - return new VectorStoreRecordDefinition(keyFields.iterator().next(), dataFields, - vectorFields); - } - - /** - * Create a VectorStoreRecordDefinition from a collection of fields. - * @param fields The fields to create the definition from. - * @return VectorStoreRecordDefinition - */ - public static VectorStoreRecordDefinition fromFields(List fields) { - List keyFields = fields.stream() - .filter(p -> p instanceof VectorStoreRecordKeyField) - .map(p -> (VectorStoreRecordKeyField) p) - .collect(Collectors.toList()); - - List dataFields = fields.stream() - .filter(p -> p instanceof VectorStoreRecordDataField) - .map(p -> (VectorStoreRecordDataField) p) - .collect(Collectors.toList()); - - List vectorFields = fields.stream() - .filter(p -> p instanceof VectorStoreRecordVectorField) - .map(p -> (VectorStoreRecordVectorField) p) - .collect(Collectors.toList()); - - return checkFields(keyFields, dataFields, vectorFields); - } - - /** - * Create a VectorStoreRecordDefinition from a model. - * @param recordClass The model class to create the definition from. - * @return VectorStoreRecordDefinition - */ - public static VectorStoreRecordDefinition fromRecordClass(Class recordClass) { - List keyFields = new ArrayList<>(); - List dataFields = new ArrayList<>(); - List vectorFields = new ArrayList<>(); - - for (Field field : recordClass.getDeclaredFields()) { - if (field.isAnnotationPresent(VectorStoreRecordKeyAttribute.class)) { - VectorStoreRecordKeyAttribute keyAttribute = field - .getAnnotation(VectorStoreRecordKeyAttribute.class); - - keyFields.add(VectorStoreRecordKeyField.builder() - .withName(field.getName()) - .withStorageName(keyAttribute.storageName()) - .build()); - } - - if (field.isAnnotationPresent(VectorStoreRecordDataAttribute.class)) { - VectorStoreRecordDataAttribute dataAttribute = field - .getAnnotation(VectorStoreRecordDataAttribute.class); - - dataFields.add(VectorStoreRecordDataField.builder() - .withName(field.getName()) - .withStorageName(dataAttribute.storageName()) - .withHasEmbedding(dataAttribute.hasEmbedding()) - .withEmbeddingFieldName(dataAttribute.embeddingFieldName()) - .withFieldType(field.getType()) - .withIsFilterable(dataAttribute.isFilterable()) - .build()); - } - - if (field.isAnnotationPresent(VectorStoreRecordVectorAttribute.class)) { - VectorStoreRecordVectorAttribute vectorAttribute = field - .getAnnotation(VectorStoreRecordVectorAttribute.class); - - vectorFields.add(VectorStoreRecordVectorField.builder() - .withName(field.getName()) - .withStorageName(vectorAttribute.storageName()) - .withDimensions(vectorAttribute.dimensions()) - .withIndexKind(IndexKind.fromString(vectorAttribute.indexKind())) - .withDistanceFunction( - DistanceFunction.fromString(vectorAttribute.distanceFunction())) - .build()); - } - } - - return checkFields(keyFields, dataFields, vectorFields); - } - - public static void validateSupportedTypes(List declaredFields, - Set> supportedTypes) { - Set> unsupportedTypes = new HashSet<>(); - for (Field declaredField : declaredFields) { - if (!supportedTypes.contains(declaredField.getType())) { - unsupportedTypes.add(declaredField.getType()); - } - } - if (!unsupportedTypes.isEmpty()) { - throw new IllegalArgumentException( - String.format( - "Unsupported field types found in record class: %s. Supported types: %s", - unsupportedTypes.stream().map(Class::getName).collect(Collectors.joining(", ")), - supportedTypes.stream().map(Class::getName).collect(Collectors.joining(", ")))); - } - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordField.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordField.java deleted file mode 100644 index c27c7a069..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recorddefinition/VectorStoreRecordField.java +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recorddefinition; - -import com.microsoft.semantickernel.builders.SemanticKernelBuilder; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Represents a field in a record. - */ -public class VectorStoreRecordField { - private final String name; - @Nullable - private final String storageName; - - /** - * Creates a new instance of the VectorStoreRecordField class. - * - * @param name the name of the field - */ - public VectorStoreRecordField(@Nonnull String name, - @Nullable String storageName) { - this.name = name; - this.storageName = storageName; - } - - /** - * Gets the name of the field. - * - * @return the name of the field - */ - public String getName() { - return name; - } - - /** - * Gets the storage name of the field. - * - * @return the storage name of the field - */ - @Nullable - public String getStorageName() { - return storageName; - } - - public abstract static class Builder> - implements SemanticKernelBuilder { - protected String name = ""; - protected String storageName = ""; - - /** - * Sets the name of the field. - * - * @param name the name of the field - * @return the builder - */ - public U withName(String name) { - this.name = name; - return (U) this; - } - - /** - * Sets the storage name of the field. - * - * @param storageName the storage name of the field - * @return the builder - */ - public U withStorageName(String storageName) { - this.storageName = storageName; - return (U) this; - } - - /** - * Builds the field. - * - * @return the field - */ - @Override - public abstract T build(); - } -} diff --git a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/GetRecordOptions.java b/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/GetRecordOptions.java deleted file mode 100644 index 6c81b5c96..000000000 --- a/semantickernel-experimental/src/main/java/com/microsoft/semantickernel/data/recordoptions/GetRecordOptions.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data.recordoptions; - -import com.microsoft.semantickernel.builders.SemanticKernelBuilder; - -/** - * Options for getting a record. - */ -public class GetRecordOptions { - private final boolean includeVectors; - - private GetRecordOptions(boolean includeVectors) { - this.includeVectors = includeVectors; - } - - /** - * Creates a new builder. - * - * @return the builder - */ - public static Builder builder() { - return new Builder(); - } - - public static class Builder implements SemanticKernelBuilder { - private boolean includeVectors; - - /** - * Sets whether to include vectors. - * - * @param includeVectors whether to include vectors - * @return GetRecordOptions.Builder - */ - public Builder includeVectors(boolean includeVectors) { - this.includeVectors = includeVectors; - return this; - } - - /** - * Builds the options. - * - * @return GetRecordOptions - */ - @Override - public GetRecordOptions build() { - return new GetRecordOptions(includeVectors); - } - } - - /** - * Gets whether to include vectors. - * - * @return whether to include vectors - */ - public boolean includeVectors() { - return includeVectors; - } -} diff --git a/semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/Hotel.java b/semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/Hotel.java deleted file mode 100644 index 6e895714f..000000000 --- a/semantickernel-experimental/src/test/java/com/microsoft/semantickernel/data/Hotel.java +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel.data; - -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordDataAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordKeyAttribute; -import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordVectorAttribute; - -import java.util.List; - -public class Hotel { - @VectorStoreRecordKeyAttribute - private final String id; - @VectorStoreRecordDataAttribute - private final String name; - @VectorStoreRecordDataAttribute - private final int code; - @VectorStoreRecordDataAttribute(hasEmbedding = true, embeddingFieldName = "descriptionEmbedding") - private final String description; - @VectorStoreRecordVectorAttribute(dimensions = 3) - private final List descriptionEmbedding; - @VectorStoreRecordDataAttribute - private final double rating; - - public Hotel() { - this(null, null, 0, null, null, 0.0); - } - - public Hotel(String id, String name, int code, String description, - List descriptionEmbedding, double rating) { - this.id = id; - this.name = name; - this.code = code; - this.description = description; - this.descriptionEmbedding = descriptionEmbedding; - this.rating = rating; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public int getCode() { - return code; - } - - public String getDescription() { - return description; - } - - public List getDescriptionEmbedding() { - return descriptionEmbedding; - } - - public double getRating() { - return rating; - } -}