From a720809a8928d7671e54dd9385d969c7d5137ad8 Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Wed, 7 Jun 2023 17:45:02 +0100 Subject: [PATCH] Java - Add ChatCompletions support (#1221) ### Motivation and Context ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows SK Contribution Guidelines (https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) - [ ] The code follows the .NET coding conventions (https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions) verified with `dotnet format` - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .github/workflows/java-build.yml | 57 +- .github/workflows/java-format.yml | 80 ++ .github/workflows/java-integration-tests.yml | 66 +- .../e2e/TextEmbeddingsTest.java | 1 - java/pom.xml | 19 +- java/samples/pom.xml | 110 +- java/samples/sample-code/pom.xml | 65 ++ .../com/microsoft/semantickernel/Config.java | 0 .../Example00GettingStarted.java | 0 .../Example02RunnningPromptsFromFile.java | 0 .../Example03SemanticFunctionInline.java | 0 .../Example04ContextVariablesChat.java | 0 .../Example05UsingThePlanner.java | 0 .../Example06MemoryAndEmbeddings.java | 0 .../Example01_NativeFunctions.java | 0 .../syntaxexamples/Example02_Pipeline.java | 0 .../Example06_TemplateLanguage.java | 3 - .../Example08_RetryHandler.java | 0 .../syntaxexamples/Example17_ChatGPT.java | 60 ++ java/samples/semantickernel-guice/pom.xml | 50 + .../guice/CompletionFunctionFactory.java | 27 + .../semantickernel/guice/Config.java | 70 ++ .../guice/SemanticKernelModule.java | 131 +++ .../guice/CompletionFunctionExample.java | 80 ++ .../guice/GuiceEmbeddingExample.java | 45 + .../semantickernel/KernelConfig.java | 52 + .../com/microsoft/semantickernel/Verify.java | 8 + .../ai/embeddings/Embedding.java | 32 +- .../builders/BuildersSingleton.java | 12 + .../semantickernel/builders/SKBuilders.java | 5 + .../chatcompletion/ChatCompletion.java | 32 + .../chatcompletion/ChatHistory.java | 83 ++ .../chatcompletion/ChatRequestSettings.java | 7 + .../semantickernel/memory/MemoryRecord.java | 23 + .../memory/MemoryRecordMetadata.java | 49 + .../semantickernel/memory/MemoryStore.java | 2 +- .../ai/openai/azuresdk/ClientBase.java | 11 + .../chatcompletion/OpenAIChatCompletion.java | 137 +++ .../chatcompletion/OpenAIChatHistory.java | 50 + ...rnel.chatcompletion.ChatCompletion$Builder | 1 + .../semantickernel/DefaultKernel.java | 17 +- .../semantickernel/DefaultKernelBuilder.java | 31 - .../memory/VolatileMemoryStore.java | 14 +- ...oft.semantickernel.Kernel$InternalBuilder} | 0 ...rnel.textcompletion.TextCompletion$Builder | 1 + .../OpenAIChatCompletionTest.java | 100 ++ .../memory/VolatileMemoryStoreTests.java | 989 ++++++++++-------- .../syntaxexamples/Example17ChatGPTTest.java | 102 ++ 48 files changed, 2006 insertions(+), 616 deletions(-) create mode 100644 .github/workflows/java-format.yml create mode 100644 java/samples/sample-code/pom.xml rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/Config.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/Example00GettingStarted.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/Example02RunnningPromptsFromFile.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/Example03SemanticFunctionInline.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/Example04ContextVariablesChat.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/Example05UsingThePlanner.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/Example06MemoryAndEmbeddings.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example01_NativeFunctions.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example02_Pipeline.java (100%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example06_TemplateLanguage.java (94%) rename java/samples/{ => sample-code}/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example08_RetryHandler.java (100%) create mode 100644 java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example17_ChatGPT.java create mode 100644 java/samples/semantickernel-guice/pom.xml create mode 100644 java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/CompletionFunctionFactory.java create mode 100644 java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/Config.java create mode 100644 java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/SemanticKernelModule.java create mode 100644 java/samples/semantickernel-guice/src/test/java/com/microsoft/semantickernel/guice/CompletionFunctionExample.java create mode 100644 java/samples/semantickernel-guice/src/test/java/com/microsoft/semantickernel/guice/GuiceEmbeddingExample.java create mode 100644 java/semantickernel-api/src/main/java/com/microsoft/semantickernel/Verify.java create mode 100644 java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatCompletion.java create mode 100644 java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatHistory.java create mode 100644 java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatRequestSettings.java create mode 100644 java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatCompletion.java create mode 100644 java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatHistory.java create mode 100644 java/semantickernel-connectors-parent/semantickernel-connectors/src/main/resources/META-INF/services/com.microsoft.semantickernel.chatcompletion.ChatCompletion$Builder delete mode 100644 java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernelBuilder.java rename java/semantickernel-core/src/main/resources/META-INF/services/{com.microsoft.semantickernel.Kernel.InternalBuilder => com.microsoft.semantickernel.Kernel$InternalBuilder} (100%) create mode 100644 java/semantickernel-core/src/main/resources/META-INF/services/com.microsoft.semantickernel.textcompletion.TextCompletion$Builder create mode 100644 java/semantickernel-core/src/test/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatCompletionTest.java create mode 100644 java/semantickernel-core/src/test/java/com/microsoft/semantickernel/syntaxexamples/Example17ChatGPTTest.java diff --git a/.github/workflows/java-build.yml b/.github/workflows/java-build.yml index 194d2dcb75..ccbf1b0285 100644 --- a/.github/workflows/java-build.yml +++ b/.github/workflows/java-build.yml @@ -1,24 +1,61 @@ name: Build Java Semantic Kernel on: + workflow_dispatch: push: - branches: [ "experimental-java", "java-dev" ] + branches: [ "experimental-java" ] + paths: + - 'java/**' pull_request: - branches: [ "experimental-java", "java-dev" ] + branches: [ "experimental-java" ] + paths: + - 'java/**' + +permissions: + contents: read jobs: - build: - + java-build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java-versions: [8, 17] + + name: Java CI on JDK${{ matrix.java-versions }} steps: - - uses: actions/checkout@v3 - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Checkout + uses: actions/checkout@v3 + + # Need to use JDK 11 to build for JDK 8 + - name: Set JDK + id: set-jdk + shell: bash + run: | + if [[ ${{ matrix.java-versions }} == 8 ]]; then + echo "JDK_VERSION=11" >> $GITHUB_OUTPUT + else + version=${{ matrix.java-versions }} + echo "JDK_VERSION=$version" >> $GITHUB_OUTPUT + fi + + - uses: actions/setup-java@v3 with: - java-version: '11' - distribution: 'temurin' + java-version: ${{ steps.set-jdk.outputs.JDK_VERSION }} + distribution: microsoft cache: maven - name: Build with Maven - run: cd java && ./mvnw -B -Pbug-check clean install --file pom.xml + run: ./mvnw -B -Pbug-check -DskipTests -Pcompile-jdk${{ matrix.java-versions }} clean install --file pom.xml + working-directory: java + + - name: Run tests + run: ./mvnw -B -Pbug-check -Pcompile-jdk${{ matrix.java-versions }} test --file pom.xml + working-directory: java + + - uses: actions/upload-artifact@v2 + if: always() + with: + name: test_output_sk_jdk${{ matrix.java-versions }}u + path: java/**/target/surefire-reports/*Test.txt diff --git a/.github/workflows/java-format.yml b/.github/workflows/java-format.yml new file mode 100644 index 0000000000..1507f33084 --- /dev/null +++ b/.github/workflows/java-format.yml @@ -0,0 +1,80 @@ +name: Java format automation +on: + issue_comment: + types: [ created ] + +jobs: + comment-driven-automation: + if: | + github.event.issue.pull_request && + ( + startsWith(github.event.comment.body, '/spotless') || + startsWith(github.event.comment.body, '/help') + ) + + runs-on: ubuntu-latest + + permissions: + issues: write + pull-requests: write + + steps: + - name: Get command + env: + BODY: ${{ github.event.comment.body }} + run: | + # intentionally only looking at the first line of the body + command=$(echo "$BODY" | head -1 | sed "s;^/;;") + echo "COMMAND=$command" >> $GITHUB_ENV + + - uses: actions/checkout@v3 + + - name: Check out PR branch + env: + NUMBER: ${{ github.event.issue.number }} + GH_TOKEN: ${{ github.token }} + run: | + gh pr checkout $NUMBER + if: env.COMMAND == 'spotless' + + - name: Set up Maven cache + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: microsoft + cache: maven + if: env.COMMAND == 'spotless' + + - name: Set git user + run: | + git config user.name github-actions[bot] + git config user.email github-action[bot]@users.noreply.github.com + if: env.COMMAND == 'spotless' + + - name: Run command + env: + NUMBER: ${{ github.event.issue.number }} + GH_TOKEN: ${{ github.token }} + run: | + available_commands="Available commands: + * \`/spotless\` - runs \`./mvnw process-sources -DskipTests -Pbug-check\` + * \`/help\` - displays available commands + " + if [[ "$COMMAND" == "spotless" ]]; then + ./mvnw process-sources -DskipTests -Pbug-check + if git diff --quiet; then + gh pr comment $NUMBER --body "Already up-to-date" + exit 0 # success + fi + git commit -a -m "./mvnw process-sources -DskipTests -Pbug-check" + git push + elif [[ "$COMMAND" == "help" ]]; then + gh pr comment $NUMBER --body "$available_commands" + else + body="Unknown command: \`$COMMAND\` + + $available_commands + " + gh pr comment $NUMBER --body "$body" + fi + working-directory: java \ No newline at end of file diff --git a/.github/workflows/java-integration-tests.yml b/.github/workflows/java-integration-tests.yml index e5751ee13a..6d84686c19 100644 --- a/.github/workflows/java-integration-tests.yml +++ b/.github/workflows/java-integration-tests.yml @@ -1,30 +1,68 @@ name: Run Java Integration Tests and Samples on: + workflow_dispatch: push: - branches: [ "experimental-java", "java-dev" ] + branches: [ "experimental-java" ] + paths: + - 'java/**' pull_request: - branches: [ "experimental-java", "java-dev" ] + branches: [ "experimental-java" ] + paths: + - 'java/**' + +permissions: + contents: read jobs: - build: - + java-integration-tests: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java-versions: [8, 17] + + name: Java Tests on JDK${{ matrix.java-versions }} steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: Checkout + uses: actions/checkout@v3 + + # Need to use JDK 11 to build for JDK 8 + - name: Set JDK + id: set-jdk + shell: bash + run: | + if [[ ${{ matrix.java-versions }} == 8 ]]; then + echo "JDK_VERSION=11" >> $GITHUB_OUTPUT + else + version=${{ matrix.java-versions }} + echo "JDK_VERSION=$version" >> $GITHUB_OUTPUT + fi + + - uses: actions/setup-java@v3 with: - java-version: '17' - distribution: 'temurin' + java-version: ${{ steps.set-jdk.outputs.JDK_VERSION }} + distribution: microsoft cache: maven - name: Build with Maven - run: cd java && ./mvnw -B -DskipTests -Pbug-check clean install --file pom.xml + run: ./mvnw -B -Pbug-check -DskipTests -Pcompile-jdk${{ matrix.java-versions }} clean install --file pom.xml + working-directory: java - - name: Run Tests - run: cd java/api-test && ../mvnw -B -Pbug-check clean install --file pom.xml + # API tests run on JDK 17+ + - name: Run integration tests + run: ../mvnw -B -Pbug-check clean install --file pom.xml + working-directory: java/api-test + env: + OPENAI_API_KEY: "" + AZURE_OPENAI_ENDPOINT: "" + AZURE_OPENAI_API_KEY: "" + AZURE_OPENAI_DEPLOYMENT_NAME: "" + if: ${{ matrix.java-versions >= 17 }} - - name: Build samples - run: cd java/samples && ../mvnw -B clean install --file pom.xml + # Samples build on JDK 17+ + - name: Build semantic-kernel samples + run: ../mvnw -B clean install --file pom.xml + working-directory: java/samples + if: ${{ matrix.java-versions >= 17 }} diff --git a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/TextEmbeddingsTest.java b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/TextEmbeddingsTest.java index af3e307c73..e9fb347dcf 100644 --- a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/TextEmbeddingsTest.java +++ b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/e2e/TextEmbeddingsTest.java @@ -13,7 +13,6 @@ import com.microsoft.semantickernel.memory.SemanticTextMemory; import com.microsoft.semantickernel.memory.VolatileMemoryStore; import com.microsoft.semantickernel.textcompletion.CompletionSKContext; import com.microsoft.semantickernel.textcompletion.CompletionSKFunction; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIf; diff --git a/java/pom.xml b/java/pom.xml index 84ecabd5db..212b6a142e 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -519,13 +519,28 @@ + + compile-jdk8 + + 1.8 + 1.8 + + + + compile-jdk17 + + true + + + 17 + 17 + + bug-check - false - diff --git a/java/samples/pom.xml b/java/samples/pom.xml index af12925b41..d0ea1c41d6 100644 --- a/java/samples/pom.xml +++ b/java/samples/pom.xml @@ -1,13 +1,18 @@ - - + + 4.0.0 com.microsoft.semantickernel + samples 0.2.0-SNAPSHOT - + pom + + + sample-code + semantickernel-guice + 0.10.10 @@ -29,97 +34,4 @@ - - - - com.microsoft.semantickernel - semantickernel-api - - - com.microsoft.semantickernel.connectors - semantickernel-connectors - - - com.microsoft.semantickernel - openai-sdk - - - com.microsoft.semantickernel - semantickernel-core - runtime - - - com.microsoft.semantickernel - semantickernel-core-skills - ${project.version} - - - org.apache.logging.log4j - log4j-api - ${log4j2.version} - - - org.apache.logging.log4j - log4j-core - ${log4j2.version} - - - org.apache.logging.log4j - log4j-slf4j2-impl - ${log4j2.version} - - - com.microsoft.semantickernel.extensions - sequentialplanner-extensions - compile - - - - - - bug-check - - - false - - - - - - org.apache.maven.plugins - maven-compiler-plugin - {maven.compiler-plugin.version} - - ${maven.compiler.release} - ${maven.compiler.release} - ${project.build.sourceEncoding} - true - - -XDcompilePolicy=simple - - - -Xplugin:ErrorProne - -XepOpt:NullAway:AnnotatedPackages=com.microsoft.semantickernel - -Xep:AlmostJavadoc:OFF -Xep:MissingSummary:OFF - -Xep:UnusedVariable:OFF -Xep:EmptyBlockTag:OFF - - - - - com.google.errorprone - error_prone_core - ${google.errorprone.core.version} - - - com.uber.nullaway - nullaway - ${com.uber.nullaway.version} - - - - - - - - - \ No newline at end of file + diff --git a/java/samples/sample-code/pom.xml b/java/samples/sample-code/pom.xml new file mode 100644 index 0000000000..a6a30adc82 --- /dev/null +++ b/java/samples/sample-code/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + com.microsoft.semantickernel + samples + 0.2.0-SNAPSHOT + ../pom.xml + + + com.microsoft.semantickernel + sample-code + + jar + + + + com.microsoft.semantickernel + semantickernel-api + + + com.microsoft.semantickernel.connectors + semantickernel-connectors + + + com.microsoft.semantickernel + openai-sdk + + + com.microsoft.semantickernel + semantickernel-core + runtime + + + com.microsoft.semantickernel + semantickernel-core-skills + ${project.version} + + + + + org.apache.logging.log4j + log4j-api + 2.20.0 + + + org.apache.logging.log4j + log4j-core + 2.20.0 + + + org.apache.logging.log4j + log4j-slf4j2-impl + 2.20.0 + + + com.microsoft.semantickernel.extensions + sequentialplanner-extensions + compile + + + + \ No newline at end of file diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/Config.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Config.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/Config.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Config.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/Example00GettingStarted.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example00GettingStarted.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/Example00GettingStarted.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example00GettingStarted.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/Example02RunnningPromptsFromFile.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example02RunnningPromptsFromFile.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/Example02RunnningPromptsFromFile.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example02RunnningPromptsFromFile.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/Example03SemanticFunctionInline.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example03SemanticFunctionInline.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/Example03SemanticFunctionInline.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example03SemanticFunctionInline.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/Example04ContextVariablesChat.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example04ContextVariablesChat.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/Example04ContextVariablesChat.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example04ContextVariablesChat.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/Example05UsingThePlanner.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example05UsingThePlanner.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/Example05UsingThePlanner.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example05UsingThePlanner.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/Example06MemoryAndEmbeddings.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example06MemoryAndEmbeddings.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/Example06MemoryAndEmbeddings.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example06MemoryAndEmbeddings.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example01_NativeFunctions.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example01_NativeFunctions.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example01_NativeFunctions.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example01_NativeFunctions.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example02_Pipeline.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example02_Pipeline.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example02_Pipeline.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example02_Pipeline.java diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example06_TemplateLanguage.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example06_TemplateLanguage.java similarity index 94% rename from java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example06_TemplateLanguage.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example06_TemplateLanguage.java index 7aeee22a5b..1691c2f74c 100644 --- a/java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example06_TemplateLanguage.java +++ b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example06_TemplateLanguage.java @@ -6,10 +6,7 @@ import com.microsoft.semantickernel.Config; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.KernelConfig; import com.microsoft.semantickernel.builders.SKBuilders; -import com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion; -import com.microsoft.semantickernel.connectors.ai.openai.textembeddings.OpenAITextEmbeddingGeneration; import com.microsoft.semantickernel.coreskills.TimeSkill; -import com.microsoft.semantickernel.memory.NullMemory; import com.microsoft.semantickernel.orchestration.SKContext; import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; import com.microsoft.semantickernel.templateengine.PromptTemplateEngine; diff --git a/java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example08_RetryHandler.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example08_RetryHandler.java similarity index 100% rename from java/samples/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example08_RetryHandler.java rename to java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example08_RetryHandler.java diff --git a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example17_ChatGPT.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example17_ChatGPT.java new file mode 100644 index 0000000000..15fd638990 --- /dev/null +++ b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/syntaxexamples/Example17_ChatGPT.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.syntaxexamples; + + +import com.microsoft.openai.OpenAIAsyncClient; +import com.microsoft.semantickernel.Config; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.KernelConfig; +import com.microsoft.semantickernel.builders.SKBuilders; +import com.microsoft.semantickernel.chatcompletion.ChatCompletion; +import com.microsoft.semantickernel.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatHistory; + +/** + * The following example shows how to use Semantic Kernel with OpenAI ChatGPT API + */ +public class Example17_ChatGPT { + + public static void main(String[] args) { + OpenAIAsyncClient client = Config.getClient(true); + + KernelConfig kernelConfig = SKBuilders.kernelConfig() + .addChatCompletionService( + "chat-test", + kernel -> SKBuilders.chatCompletion().build(client, "chat-test") + ) + .build(); + + Kernel kernel = SKBuilders.kernel().setKernelConfig(kernelConfig).build(); + + ChatCompletion chatGPT = kernel.getService(null, ChatCompletion.class); + + OpenAIChatHistory chatHistory = chatGPT.createNewChat("You are a librarian, expert about books"); + + // First user message + chatHistory.addUserMessage("Hi, I'm looking for book suggestions"); + messageOutputAsync(chatHistory); + + // First bot assistant message + String reply = chatGPT.generateMessageAsync(chatHistory, null).block(); + chatHistory.addAssistantMessage(reply); + messageOutputAsync(chatHistory); + + // Second user message + chatHistory.addUserMessage("I love history and philosophy, I'd like to learn something new about Greece, any suggestion?"); + messageOutputAsync(chatHistory); + + // Second bot assistant message + reply = chatGPT.generateMessageAsync(chatHistory, null).block(); + chatHistory.addAssistantMessage(reply); + messageOutputAsync(chatHistory); + } + + private static void messageOutputAsync(ChatHistory chatHistory) { + ChatHistory.Message message = chatHistory.getMessages().get(chatHistory.getMessages().size() - 1); + + System.out.println(message.getAuthorRoles() + ": " + message.getContent()); + System.out.println("------------------------"); + } +} diff --git a/java/samples/semantickernel-guice/pom.xml b/java/samples/semantickernel-guice/pom.xml new file mode 100644 index 0000000000..1cd1d5a08a --- /dev/null +++ b/java/samples/semantickernel-guice/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + + com.microsoft.semantickernel + samples + 0.2.0-SNAPSHOT + ../pom.xml + + + semantickernel-guice + jar + + + + com.microsoft.semantickernel + semantickernel-core + compile + + + com.microsoft.semantickernel.connectors + semantickernel-connectors + + + com.microsoft.semantickernel + semantickernel-core-skills + + + + com.google.inject + guice + 7.0.0 + + + com.google.inject.extensions + guice-assistedinject + 7.0.0 + + + + org.junit.jupiter + junit-jupiter + 5.9.2 + test + + + \ No newline at end of file diff --git a/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/CompletionFunctionFactory.java b/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/CompletionFunctionFactory.java new file mode 100644 index 0000000000..7404e33979 --- /dev/null +++ b/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/CompletionFunctionFactory.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.guice; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; +import com.microsoft.semantickernel.textcompletion.CompletionSKFunction; + +import jakarta.inject.Inject; + +public interface CompletionFunctionFactory { + + CompletionSKFunction createFunction( + String prompt, String name, PromptTemplateConfig.CompletionConfig completionConfig); + + class CompletionFunctionFactoryImpl implements CompletionFunctionFactory { + @Inject private Kernel kernel; + + @Override + public CompletionSKFunction createFunction( + String prompt, + String name, + PromptTemplateConfig.CompletionConfig completionConfig) { + return kernel.getSemanticFunctionBuilder() + .createFunction(prompt, name, null, null, completionConfig); + } + } +} diff --git a/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/Config.java b/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/Config.java new file mode 100644 index 0000000000..97a7f2b027 --- /dev/null +++ b/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/Config.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.guice; + +import com.azure.core.credential.AzureKeyCredential; +import com.microsoft.openai.AzureOpenAIClient; +import com.microsoft.openai.OpenAIAsyncClient; +import com.microsoft.openai.OpenAIClientBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + + +// TODO Remove when the upstream configuration updates arrive +public class Config { + + private static final Logger LOGGER = LoggerFactory.getLogger(Config.class); + + public static final String OPEN_AI_CONF_PROPERTIES = + "samples/java/semantickernel-samples/conf.openai.properties"; + public static final String AZURE_CONF_PROPERTIES = + "samples/java/semantickernel-samples/conf.azure.properties"; + + public static String getOpenAIKey(String conf) { + return getConfigValue(conf, "token"); + } + + public static String getAzureOpenAIEndpoint(String conf) { + return getConfigValue(conf, "endpoint"); + } + + private static String getConfigValue(String configFile, String propertyName) { + File config = new File(configFile); + try (FileInputStream fis = new FileInputStream(config.getAbsolutePath())) { + Properties props = new Properties(); + props.load(fis); + return props.getProperty(propertyName); + } catch (IOException e) { + LOGGER.error( + "Unable to load config value " + propertyName + " from file" + configFile, e); + throw new RuntimeException(configFile + " not configured properly"); + } + } + + /** + * Returns the client that will handle AzureOpenAI or OpenAI requests. + * + * @param useAzureOpenAI whether to use AzureOpenAI or OpenAI. + * @return client to be used by the kernel. + */ + public static OpenAIAsyncClient getClient(boolean useAzureOpenAI) { + if (useAzureOpenAI) { + return new AzureOpenAIClient( + new com.azure.ai.openai.OpenAIClientBuilder() + .endpoint(Config.getAzureOpenAIEndpoint(AZURE_CONF_PROPERTIES)) + .credential( + new AzureKeyCredential( + Config.getOpenAIKey(AZURE_CONF_PROPERTIES))) + .buildAsyncClient()); + } + + return new OpenAIClientBuilder() + .setApiKey(Config.getOpenAIKey(OPEN_AI_CONF_PROPERTIES)) + .build(); + } +} diff --git a/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/SemanticKernelModule.java b/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/SemanticKernelModule.java new file mode 100644 index 0000000000..806ff85f70 --- /dev/null +++ b/java/samples/semantickernel-guice/src/main/java/com/microsoft/semantickernel/guice/SemanticKernelModule.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; +import com.google.inject.util.Providers; +import com.microsoft.openai.OpenAIAsyncClient; +import com.microsoft.semantickernel.DefaultKernel; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.KernelConfig; +import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; +import com.microsoft.semantickernel.builders.SKBuilders; +import com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion; +import com.microsoft.semantickernel.connectors.ai.openai.textembeddings.OpenAITextEmbeddingGeneration; +import com.microsoft.semantickernel.memory.NullMemory; +import com.microsoft.semantickernel.memory.SemanticTextMemory; +import com.microsoft.semantickernel.skilldefinition.DefaultSkillCollection; +import com.microsoft.semantickernel.skilldefinition.ReadOnlySkillCollection; +import com.microsoft.semantickernel.templateengine.DefaultPromptTemplateEngine; +import com.microsoft.semantickernel.templateengine.PromptTemplateEngine; + +import javax.annotation.Nullable; + +public class SemanticKernelModule extends AbstractModule { + + private final SemanticTextMemory memory; + private final KernelConfig.Builder kernelConfig; + private final ReadOnlySkillCollection skillCollection; + private final PromptTemplateEngine promptTemplateEngine; + private String textCompletionModelId; + private boolean withMemory; + private String embeddingsGenerationServiceId; + private OpenAIAsyncClient client; + + public SemanticKernelModule( + KernelConfig.Builder kernelConfig, + @Nullable SemanticTextMemory memory, + @Nullable ReadOnlySkillCollection skillCollection, + @Nullable PromptTemplateEngine promptTemplateEngine) { + this.kernelConfig = kernelConfig; + + if (memory != null) { + this.memory = memory.copy(); + } else { + this.memory = null; + } + + if (skillCollection == null) { + skillCollection = new DefaultSkillCollection(); + } + this.skillCollection = skillCollection; + + if (promptTemplateEngine == null) { + promptTemplateEngine = new DefaultPromptTemplateEngine(); + } + this.promptTemplateEngine = promptTemplateEngine; + } + + public SemanticKernelModule( + KernelConfig.Builder kernelConfig, @Nullable SemanticTextMemory memory) { + this(kernelConfig, memory, null, null); + } + + public SemanticKernelModule(KernelConfig.Builder kernelConfig) { + this(kernelConfig, null); + } + + public SemanticKernelModule() { + this(SKBuilders.kernelConfig(), null); + } + + public SemanticKernelModule withTextCompletionService(String model) { + this.textCompletionModelId = model; + return this; + } + + public SemanticKernelModule withClient(OpenAIAsyncClient client) { + this.client = client; + return this; + } + + @Override + protected void configure() { + super.configure(); + + if (client == null) { + client = Config.getClient(true); + } + + bind(OpenAIAsyncClient.class).toInstance(client); + + if (textCompletionModelId != null) { + kernelConfig.addTextCompletionService( + textCompletionModelId, + kernel -> new OpenAITextCompletion(client, textCompletionModelId)); + } + + if (embeddingsGenerationServiceId != null) { + OpenAITextEmbeddingGeneration embeddings = + new OpenAITextEmbeddingGeneration(client, embeddingsGenerationServiceId); + kernelConfig.addTextEmbeddingsGenerationService( + embeddingsGenerationServiceId, kernel -> embeddings); + bind(new TypeLiteral>() {}).toInstance(embeddings); + } + + bind(KernelConfig.class).toInstance(kernelConfig.build()); + bind(PromptTemplateEngine.class).toInstance(promptTemplateEngine); + bind(ReadOnlySkillCollection.class).toProvider(Providers.of(skillCollection)); + + if (withMemory) { + bind(SemanticTextMemory.class).toProvider(Providers.of(memory)); + } else { + bind(SemanticTextMemory.class).toInstance(NullMemory.getInstance()); + } + + bind(Kernel.class).to(DefaultKernel.class); + bind(CompletionFunctionFactory.class) + .to(CompletionFunctionFactory.CompletionFunctionFactoryImpl.class); + } + + public SemanticKernelModule withMemory() { + this.withMemory = true; + return this; + } + + public SemanticKernelModule withTextEmbeddingsGenerationService( + String embeddingsGenerationServiceId) { + this.embeddingsGenerationServiceId = embeddingsGenerationServiceId; + return this; + } +} diff --git a/java/samples/semantickernel-guice/src/test/java/com/microsoft/semantickernel/guice/CompletionFunctionExample.java b/java/samples/semantickernel-guice/src/test/java/com/microsoft/semantickernel/guice/CompletionFunctionExample.java new file mode 100644 index 0000000000..41a236f999 --- /dev/null +++ b/java/samples/semantickernel-guice/src/test/java/com/microsoft/semantickernel/guice/CompletionFunctionExample.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.guice; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; +import com.microsoft.semantickernel.textcompletion.CompletionSKContext; + +import java.util.ArrayList; + +public class CompletionFunctionExample { + + private final CompletionFunctionFactory completionFunctionFactory; + + public static void main(String[] args) { + Injector injector = + Guice.createInjector( + new SemanticKernelModule().withTextCompletionService("text-davinci-003")); + + CompletionFunctionExample example = injector.getInstance(CompletionFunctionExample.class); + example.run(); + } + + @Inject + public CompletionFunctionExample(CompletionFunctionFactory completionFunctionFactory) { + this.completionFunctionFactory = completionFunctionFactory; + } + + public void run() { + String prompt = "{{$input}}\n" + "Summarize the content above."; + + String text = + "\n" + + "Demo (ancient Greek poet)\n" + + "From Wikipedia, the free encyclopedia\n" + + "Demo or Damo (Greek: Δεμώ, Δαμώ; fl. c. AD 200) was a Greek woman of the\n" + + "Roman period, known for a single epigram, engraved upon the Colossus of\n" + + "Memnon, which bears her name. She speaks of herself therein as a lyric\n" + + "poetess dedicated to the Muses, but nothing is known of her life.[1]\n" + + "Identity\n" + + "Demo was evidently Greek, as her name, a traditional epithet of Demeter,\n" + + "signifies. The name was relatively common in the Hellenistic world, in\n" + + "Egypt and elsewhere, and she cannot be further identified. The date of her\n" + + "visit to the Colossus of Memnon cannot be established with certainty, but\n" + + "internal evidence on the left leg suggests her poem was inscribed there at\n" + + "some point in or after AD 196.[2]\n" + + "Epigram\n" + + "There are a number of graffiti inscriptions on the Colossus of Memnon.\n" + + "Following three epigrams by Julia Balbilla, a fourth epigram, in elegiac\n" + + "couplets, entitled and presumably authored by \"Demo\" or \"Damo\" (the\n" + + "Greek inscription is difficult to read), is a dedication to the Muses.[2]\n" + + "The poem is traditionally published with the works of Balbilla, though the\n" + + "internal evidence suggests a different author.[1]\n" + + "In the poem, Demo explains that Memnon has shown her special respect. In\n" + + "return, Demo offers the gift for poetry, as a gift to the hero. At the end\n" + + "of this epigram, she addresses Memnon, highlighting his divine status by\n" + + "recalling his strength and holiness.[2]\n" + + "Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic\n" + + "dialect. The language indicates she was knowledgeable in Homeric\n" + + "poetry—'bearing a pleasant gift', for example, alludes to the use of that\n" + + "phrase throughout the Iliad and Odyssey.[a][2];"; + + CompletionSKContext summary = + completionFunctionFactory + .createFunction( + prompt, + "summarize", + new PromptTemplateConfig.CompletionConfig( + 0.2, 0.5, 0, 0, 2000, new ArrayList<>())) + .invokeAsync(text) + .block(); + + if (summary != null) { + System.out.println("Result: " + summary.getResult()); + } else { + System.out.println("Null result"); + } + } +} diff --git a/java/samples/semantickernel-guice/src/test/java/com/microsoft/semantickernel/guice/GuiceEmbeddingExample.java b/java/samples/semantickernel-guice/src/test/java/com/microsoft/semantickernel/guice/GuiceEmbeddingExample.java new file mode 100644 index 0000000000..10f33dd2f1 --- /dev/null +++ b/java/samples/semantickernel-guice/src/test/java/com/microsoft/semantickernel/guice/GuiceEmbeddingExample.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.guice; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; + +import jakarta.inject.Inject; + +import java.util.ArrayList; +import java.util.List; + +public class GuiceEmbeddingExample { + + private final EmbeddingGeneration embeddingGeneration; + + public static void main(String[] args) { + Injector injector = + Guice.createInjector( + new SemanticKernelModule() + .withMemory() + .withTextEmbeddingsGenerationService("text-embedding-ada-002")); + + GuiceEmbeddingExample example = injector.getInstance(GuiceEmbeddingExample.class); + example.run(); + } + + @Inject + public GuiceEmbeddingExample(EmbeddingGeneration embeddingGeneration) { + this.embeddingGeneration = embeddingGeneration; + } + + public void run() { + List data = new ArrayList<>(); + data.add("This is just a test"); + + embeddingGeneration + .generateEmbeddingsAsync(data) + .block() + .forEach( + doubleEmbedding -> { + doubleEmbedding.getVector().forEach(System.out::println); + }); + } +} diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelConfig.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelConfig.java index 8ca513a414..c68a8ad8f3 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelConfig.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelConfig.java @@ -2,6 +2,7 @@ package com.microsoft.semantickernel; import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; +import com.microsoft.semantickernel.chatcompletion.ChatCompletion; import com.microsoft.semantickernel.orchestration.SKFunction; import com.microsoft.semantickernel.textcompletion.TextCompletion; @@ -14,6 +15,7 @@ public final class KernelConfig { private static final String DEFAULT_SERVICE_ID = "__SK_DEFAULT"; private final Map> textCompletionServices; + private final Map> chatCompletionServices; private final Map>> textEmbeddingGenerationServices; @@ -23,10 +25,12 @@ public final class KernelConfig { Map> textCompletionServices, Map>> textEmbeddingGenerationServices, + Map> chatCompletionServices, List> skills) { this.textCompletionServices = new HashMap<>(); this.textCompletionServices.putAll(textCompletionServices); this.textEmbeddingGenerationServices = new HashMap<>(textEmbeddingGenerationServices); + this.chatCompletionServices = new HashMap<>(chatCompletionServices); this.skills = new ArrayList<>(skills); } @@ -54,6 +58,21 @@ public final class KernelConfig { return this.textCompletionServices.get(serviceId); } + public Function getChatCompletionServiceOrDefault( + @Nullable String serviceId) { + if (serviceId == null) { + serviceId = DEFAULT_SERVICE_ID; + } + + if (!this.chatCompletionServices.containsKey(serviceId)) { + throw new KernelException( + KernelException.ErrorCodes.ServiceNotFound, + "A chat completion service id '" + serviceId + "' doesn't exist"); + } + + return this.chatCompletionServices.get(serviceId); + } + public static class Builder { private Map> textCompletionServices = new HashMap<>(); @@ -62,6 +81,8 @@ public final class KernelConfig { private Map>> textEmbeddingGenerationServices = new HashMap<>(); + private final Map> chatCompletionServices = + new HashMap<>(); public Builder addSkill(SKFunction functionDefinition) { skillBuilders.add(functionDefinition); @@ -111,10 +132,41 @@ public final class KernelConfig { return this; } + /** + * Add to the list a service for chat completion, e.g. OpenAI ChatGPT. + * + * @param serviceId Id used to identify the service + * @param serviceFactory Function used to instantiate the service object + * @return Current object instance + */ + public Builder addChatCompletionService( + @Nullable String serviceId, Function serviceFactory) { + if (serviceId != null && serviceId.toUpperCase().equals(DEFAULT_SERVICE_ID)) { + String msg = + "The service id '" + + serviceId + + "' is reserved, please use a different name"; + throw new KernelException( + KernelException.ErrorCodes.InvalidServiceConfiguration, msg); + } + + if (serviceId == null) { + serviceId = DEFAULT_SERVICE_ID; + } + + this.chatCompletionServices.put(serviceId, serviceFactory); + if (this.chatCompletionServices.size() == 1) { + this.chatCompletionServices.put(DEFAULT_SERVICE_ID, serviceFactory); + } + + return this; + } + public KernelConfig build() { return new KernelConfig( Collections.unmodifiableMap(textCompletionServices), textEmbeddingGenerationServices, + chatCompletionServices, skillBuilders); } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/Verify.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/Verify.java new file mode 100644 index 0000000000..2ff77e5a35 --- /dev/null +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/Verify.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel; + +public class Verify { + public static void notNull(Object object) { + assert object != null; + } +} diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/ai/embeddings/Embedding.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/ai/embeddings/Embedding.java index 9b2e5df5c7..05c0e78710 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/ai/embeddings/Embedding.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/ai/embeddings/Embedding.java @@ -4,6 +4,7 @@ package com.microsoft.semantickernel.ai.embeddings; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * Represents a strongly typed vector of numeric data. @@ -43,7 +44,7 @@ public class Embedding { // unsupported. // /// A null vector is passed in. public Embedding() { - this.vector = new ArrayList<>(); + this.vector = Collections.emptyList(); } /** @@ -54,6 +55,33 @@ public class Embedding { */ public Embedding(List vector) { // Verify.NotNull(vector, nameof(vector)); - this.vector = Collections.unmodifiableList(vector); + this.vector = + vector != null ? Collections.unmodifiableList(vector) : Collections.emptyList(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Embedding embedding = (Embedding) o; + + return vector.equals(embedding.vector); + } + + @Override + public int hashCode() { + return vector.hashCode(); + } + + @Override + public String toString() { + return "Embedding{" + + "vector=" + + vector.stream() + .limit(3) + .map(String::valueOf) + .collect(Collectors.joining(", ", "[", vector.size() > 3 ? "...]" : "]")) + + '}'; } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/BuildersSingleton.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/BuildersSingleton.java index 243121c765..a60e8090ac 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/BuildersSingleton.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/BuildersSingleton.java @@ -3,6 +3,7 @@ package com.microsoft.semantickernel.builders; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; +import com.microsoft.semantickernel.chatcompletion.ChatCompletion; import com.microsoft.semantickernel.memory.SemanticTextMemory; import com.microsoft.semantickernel.orchestration.ContextVariables; import com.microsoft.semantickernel.orchestration.SKContext; @@ -43,6 +44,8 @@ public enum BuildersSingleton { "com.microsoft.semantickernel.templateengine.DefaultPromptTemplateEngine$Builder"; private static final String FALLBACK_SEMANTIC_TEXT_MEMORY_CLASS = "com.microsoft.semantickernel.memory.DefaultSemanticTextMemory$Builder"; + private static final String FALLBACK_CHAT_COMPLETION_BUILDER_CLASS = + "com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatCompletion$Builder"; private final FunctionBuilders functionBuilders; private final Kernel.InternalBuilder kernelBuilder; @@ -53,6 +56,7 @@ public enum BuildersSingleton { private final ContextVariables.Builder variables; private final SKContext.Builder context; private final PromptTemplateEngine.Builder promptTemplateEngine; + private final ChatCompletion.Builder chatCompletion; private final SemanticTextMemory.Builder semanticTextMemoryBuilder; @@ -100,6 +104,10 @@ public enum BuildersSingleton { PromptTemplateEngine.Builder.class, FALLBACK_PROMPT_TEMPLATE_ENGINE_BUILDER_CLASS); + chatCompletion = + ServiceLoadUtil.findServiceLoader( + ChatCompletion.Builder.class, FALLBACK_CHAT_COMPLETION_BUILDER_CLASS); + } catch (Throwable e) { Logger LOGGER = LoggerFactory.getLogger(BuildersSingleton.class); LOGGER.error("Failed to discover Semantic Kernel Builders", e); @@ -164,4 +172,8 @@ public enum BuildersSingleton { public SemanticTextMemory.Builder getSemanticTextMemoryBuilder() { return semanticTextMemoryBuilder; } + + public ChatCompletion.Builder getChatCompletion() { + return chatCompletion; + } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SKBuilders.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SKBuilders.java index b2f1f23d4f..976fa2a7f9 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SKBuilders.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/builders/SKBuilders.java @@ -4,6 +4,7 @@ package com.microsoft.semantickernel.builders; import com.microsoft.semantickernel.Kernel; import com.microsoft.semantickernel.KernelConfig; import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration; +import com.microsoft.semantickernel.chatcompletion.ChatCompletion; import com.microsoft.semantickernel.memory.SemanticTextMemory; import com.microsoft.semantickernel.orchestration.ContextVariables; import com.microsoft.semantickernel.orchestration.SKContext; @@ -60,4 +61,8 @@ public class SKBuilders { public static SKContext.Builder context() { return BuildersSingleton.INST.context(); } + + public static ChatCompletion.Builder chatCompletion() { + return BuildersSingleton.INST.getChatCompletion(); + } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatCompletion.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatCompletion.java new file mode 100644 index 0000000000..7108c2992e --- /dev/null +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatCompletion.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.chatcompletion; + +import com.microsoft.openai.OpenAIAsyncClient; + +import reactor.core.publisher.Mono; + +import javax.annotation.Nullable; + +public interface ChatCompletion { + /** + * Generate a new chat message + * + * @param chat Chat history + * @param requestSettings AI request settings + * @return Generated chat message in string format + */ + Mono generateMessageAsync( + ChatHistoryType chat, @Nullable ChatRequestSettings requestSettings); + + /** + * Create a new empty chat instance + * + * @param instructions Optional chat instructions for the AI service + * @return Chat object + */ + ChatHistoryType createNewChat(@Nullable String instructions); + + interface Builder { + ChatCompletion build(OpenAIAsyncClient client, String modelId); + } +} diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatHistory.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatHistory.java new file mode 100644 index 0000000000..9314e8496c --- /dev/null +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatHistory.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.chatcompletion; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** Provides a history of messages between the User, Assistant and System */ +public class ChatHistory { + + private final List messages; + + public ChatHistory() { + this.messages = new ArrayList<>(); + } + + /** + * @return List of messages in the chat + */ + public List getMessages() { + return Collections.unmodifiableList(messages); + } + + /** + * @return The most recent message in chat + */ + public Optional getLastMessage() { + if (messages.isEmpty()) { + return Optional.empty(); + } + return Optional.of(messages.get(messages.size() - 1)); + } + + public enum AuthorRoles { + Unknown, + System, + User, + Assistant + } + + /** Chat message representation */ + public static class Message { + private final AuthorRoles authorRoles; + + private final String content; + + /** + * Create a new instance + * + * @param authorRoles Role of message author + * @param content Message content + */ + public Message(AuthorRoles authorRoles, String content) { + this.authorRoles = authorRoles; + this.content = content; + } + + /** + * @return Role of the message author, e.g. user/assistant/system + */ + public AuthorRoles getAuthorRoles() { + return authorRoles; + } + + /** + * @return Message content + */ + public String getContent() { + return content; + } + } + + /** + * Add a message to the chat history + * + * @param authorRole Role of the message author + * @param content Message content + */ + public void addMessage(AuthorRoles authorRole, String content) { + this.messages.add(new Message(authorRole, content)); + } +} diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatRequestSettings.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatRequestSettings.java new file mode 100644 index 0000000000..7c6339b95d --- /dev/null +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/chatcompletion/ChatRequestSettings.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.chatcompletion; + +import com.microsoft.semantickernel.textcompletion.CompletionRequestSettings; + +/** Settings for a chat completion request */ +public class ChatRequestSettings extends CompletionRequestSettings {} diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryRecord.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryRecord.java index 0693b892c0..dead651d72 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryRecord.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryRecord.java @@ -140,6 +140,29 @@ public class MemoryRecord extends DataEntryBase { metadata, embedding != null ? embedding : Embedding.empty(), key, timestamp); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MemoryRecord that = (MemoryRecord) o; + + if (!embedding.equals(that.embedding)) return false; + return metadata.equals(that.metadata); + } + + @Override + public int hashCode() { + int result = embedding.hashCode(); + result = 31 * result + metadata.hashCode(); + return result; + } + + @Override + public String toString() { + return "MemoryRecord{" + "embedding=" + embedding + ", metadata=" + metadata + '}'; + } + /* /// /// Create a memory record from a serialized metadata string. diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryRecordMetadata.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryRecordMetadata.java index f9e6a990f0..14e6cb35ef 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryRecordMetadata.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryRecordMetadata.java @@ -93,4 +93,53 @@ public class MemoryRecordMetadata { this.externalSourceName = externalSourceName; this.additionalMetadata = additionalMetadata; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MemoryRecordMetadata that = (MemoryRecordMetadata) o; + + if (isReference != that.isReference) return false; + if (!externalSourceName.equals(that.externalSourceName)) return false; + if (!id.equals(that.id)) return false; + if (!description.equals(that.description)) return false; + if (!text.equals(that.text)) return false; + return additionalMetadata.equals(that.additionalMetadata); + } + + @Override + public int hashCode() { + int result = (isReference ? 1 : 0); + result = 31 * result + externalSourceName.hashCode(); + result = 31 * result + id.hashCode(); + result = 31 * result + description.hashCode(); + result = 31 * result + text.hashCode(); + result = 31 * result + additionalMetadata.hashCode(); + return result; + } + + @Override + public String toString() { + return "MemoryRecordMetadata{" + + "isReference=" + + isReference + + ", externalSourceName='" + + externalSourceName + + '\'' + + ", id='" + + id + + '\'' + + ", description='" + + description + + '\'' + + ", text='" + + text + + '\'' + + ", additionalMetadata='" + + additionalMetadata + + '\'' + + '}'; + } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryStore.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryStore.java index 62f9e98db2..0a6013144c 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryStore.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/memory/MemoryStore.java @@ -143,7 +143,7 @@ public interface MemoryStore { */ Mono> getNearestMatchAsync( @Nonnull String collectionName, - @Nonnull Embedding embedding, + @Nonnull Embedding embedding, double minRelevanceScore, boolean withEmbedding); } diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/azuresdk/ClientBase.java b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/azuresdk/ClientBase.java index 1152d2f7c4..f07b88968e 100644 --- a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/azuresdk/ClientBase.java +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/azuresdk/ClientBase.java @@ -2,6 +2,7 @@ package com.microsoft.semantickernel.connectors.ai.openai.azuresdk; import com.microsoft.openai.OpenAIAsyncClient; +import com.microsoft.semantickernel.ai.AIException; public abstract class ClientBase { private final String modelId; @@ -19,4 +20,14 @@ public abstract class ClientBase { protected OpenAIAsyncClient getClient() { return client; } + + protected static void validateMaxTokens(int maxTokens) { + if (maxTokens < 1) { + throw new AIException( + AIException.ErrorCodes.InvalidRequest, + "MaxTokens " + + maxTokens + + " is not valid, the value must be greater than zero"); + } + } } diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatCompletion.java b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatCompletion.java new file mode 100644 index 0000000000..e4ce866293 --- /dev/null +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatCompletion.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.connectors.ai.openai.chatcompletion; + +import com.azure.ai.openai.models.ChatCompletionsOptions; +import com.azure.ai.openai.models.ChatMessage; +import com.azure.ai.openai.models.ChatRole; +import com.microsoft.openai.OpenAIAsyncClient; +import com.microsoft.semantickernel.Verify; +import com.microsoft.semantickernel.ai.AIException; +import com.microsoft.semantickernel.chatcompletion.ChatCompletion; +import com.microsoft.semantickernel.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.chatcompletion.ChatRequestSettings; +import com.microsoft.semantickernel.connectors.ai.openai.azuresdk.ClientBase; + +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +/** OpenAI chat completion client. */ +public class OpenAIChatCompletion extends ClientBase implements ChatCompletion { + public OpenAIChatCompletion(OpenAIAsyncClient client, String modelId) { + super(client, modelId); + } + + public static class Builder implements ChatCompletion.Builder { + public Builder() {} + + public ChatCompletion build(OpenAIAsyncClient client, String modelId) { + return new OpenAIChatCompletion(client, modelId); + } + } + + /** + * Generate a new chat message + * + * @param chat Chat history + * @param requestSettings AI request settings + * @return The response generated by the request + */ + public Mono generateMessageAsync( + OpenAIChatHistory chat, @Nullable ChatRequestSettings requestSettings) { + + if (requestSettings == null) { + requestSettings = new ChatRequestSettings(); + } + return this.internalGenerateChatMessageAsync(chat, requestSettings); + } + + /** + * Generate a new chat message + * + * @param chat Chat history + * @param requestSettings AI request settings + * @return The response generated by the request + */ + private Mono internalGenerateChatMessageAsync( + ChatHistory chat, ChatRequestSettings requestSettings) { + Verify.notNull(chat); + Verify.notNull(requestSettings); + + validateMaxTokens(requestSettings.getMaxTokens()); + ChatCompletionsOptions options = createChatCompletionsOptions(requestSettings, chat); + + return getClient() + .getChatCompletions(getModelId(), options) + .flatMap( + response -> { + if (response == null || response.getChoices().isEmpty()) { + return Mono.error( + new AIException( + AIException.ErrorCodes.InvalidResponseContent, + "Chat completions not found")); + } else { + return Mono.just( + response.getChoices().get(0).getMessage().getContent()); + } + }); + } + + private static ChatCompletionsOptions createChatCompletionsOptions( + ChatRequestSettings requestSettings, ChatHistory chat) { + List messages = + chat.getMessages().stream() + .map( + it -> { + return new ChatMessage(toChatRole(it.getAuthorRoles())) + .setContent(it.getContent()); + }) + .collect(Collectors.toList()); + + ChatCompletionsOptions options = new ChatCompletionsOptions(messages); + + options.setMaxTokens(requestSettings.getMaxTokens()); + options.setTemperature(requestSettings.getTemperature()); + options.setTopP(requestSettings.getTopP()); + options.setFrequencyPenalty(requestSettings.getFrequencyPenalty()); + options.setPresencePenalty(requestSettings.getPresencePenalty()); + options.setN(1); + + return options; + } + + private static ChatRole toChatRole(ChatHistory.AuthorRoles authorRoles) { + switch (authorRoles) { + case System: + return ChatRole.SYSTEM; + case User: + return ChatRole.USER; + case Assistant: + return ChatRole.ASSISTANT; + default: + throw new IllegalArgumentException( + "Invalid chat message author: " + authorRoles.name()); + } + } + + @Override + public OpenAIChatHistory createNewChat(@Nullable String instructions) { + return internalCreateNewChat(instructions); + } + + /** + * Create a new empty chat instance + * + * @param instructions Optional chat instructions for the AI service + * @return Chat object + */ + private static OpenAIChatHistory internalCreateNewChat(@Nullable String instructions) { + if (instructions == null) { + instructions = ""; + } + return new OpenAIChatHistory(instructions); + } +} diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatHistory.java b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatHistory.java new file mode 100644 index 0000000000..e5f89bae37 --- /dev/null +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatHistory.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.connectors.ai.openai.chatcompletion; + +import com.microsoft.semantickernel.chatcompletion.ChatHistory; + +import javax.annotation.Nullable; + +/** OpenAI Chat content */ +public class OpenAIChatHistory extends ChatHistory { + + /** + * Create a new and empty chat history + * + * @param assistantInstructions Optional instructions for the assistant + */ + public OpenAIChatHistory(@Nullable String assistantInstructions) { + super(); + + if (assistantInstructions != null && !assistantInstructions.isEmpty()) { + this.addSystemMessage(assistantInstructions); + } + } + + /** + * Add a system message to the chat history + * + * @param content Message content + */ + public void addSystemMessage(String content) { + this.addMessage(AuthorRoles.System, content); + } + + /** + * Add an assistant message to the chat history + * + * @param content Message content + */ + public void addAssistantMessage(String content) { + this.addMessage(AuthorRoles.Assistant, content); + } + + /** + * Add a user message to the chat history + * + * @param content Message content + */ + public void addUserMessage(String content) { + this.addMessage(AuthorRoles.User, content); + } +} diff --git a/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/resources/META-INF/services/com.microsoft.semantickernel.chatcompletion.ChatCompletion$Builder b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/resources/META-INF/services/com.microsoft.semantickernel.chatcompletion.ChatCompletion$Builder new file mode 100644 index 0000000000..d3912ddc26 --- /dev/null +++ b/java/semantickernel-connectors-parent/semantickernel-connectors/src/main/resources/META-INF/services/com.microsoft.semantickernel.chatcompletion.ChatCompletion$Builder @@ -0,0 +1 @@ +com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatCompletion$Builder \ No newline at end of file diff --git a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernel.java b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernel.java index 2740038fa7..bdee52acc9 100644 --- a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernel.java +++ b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernel.java @@ -4,12 +4,17 @@ package com.microsoft.semantickernel; import com.microsoft.semantickernel.ai.AIException; import com.microsoft.semantickernel.builders.FunctionBuilders; import com.microsoft.semantickernel.builders.SKBuilders; +import com.microsoft.semantickernel.chatcompletion.ChatCompletion; import com.microsoft.semantickernel.coreskills.SkillImporter; import com.microsoft.semantickernel.exceptions.NotSupportedException; import com.microsoft.semantickernel.exceptions.SkillsNotFoundException; import com.microsoft.semantickernel.memory.MemoryStore; import com.microsoft.semantickernel.memory.SemanticTextMemory; -import com.microsoft.semantickernel.orchestration.*; +import com.microsoft.semantickernel.orchestration.ContextVariables; +import com.microsoft.semantickernel.orchestration.DefaultCompletionSKFunction; +import com.microsoft.semantickernel.orchestration.RegistrableSkFunction; +import com.microsoft.semantickernel.orchestration.SKContext; +import com.microsoft.semantickernel.orchestration.SKFunction; import com.microsoft.semantickernel.semanticfunctions.SemanticFunctionConfig; import com.microsoft.semantickernel.skilldefinition.DefaultSkillCollection; import com.microsoft.semantickernel.skilldefinition.FunctionNotFound; @@ -66,6 +71,16 @@ public class DefaultKernel implements Kernel { "No text completion service available"); } + return (T) factory.apply(this); + } else if (ChatCompletion.class.isAssignableFrom(clazz)) { + Function factory = + kernelConfig.getChatCompletionServiceOrDefault(serviceId); + if (factory == null) { + throw new KernelException( + KernelException.ErrorCodes.ServiceNotFound, + "No chat completion service available"); + } + return (T) factory.apply(this); } else { // TODO correct exception diff --git a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernelBuilder.java b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernelBuilder.java deleted file mode 100644 index a71f640a41..0000000000 --- a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/DefaultKernelBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -package com.microsoft.semantickernel; - -import com.microsoft.semantickernel.memory.MemoryStore; -import com.microsoft.semantickernel.memory.VolatileMemoryStore; -import com.microsoft.semantickernel.templateengine.DefaultPromptTemplateEngine; -import com.microsoft.semantickernel.templateengine.PromptTemplateEngine; - -import javax.annotation.Nullable; - -public class DefaultKernelBuilder implements Kernel.InternalBuilder { - - @Override - public Kernel build( - KernelConfig kernelConfig, - @Nullable PromptTemplateEngine promptTemplateEngine, - @Nullable MemoryStore memoryStore) { - if (promptTemplateEngine == null) { - promptTemplateEngine = new DefaultPromptTemplateEngine(); - } - - if (kernelConfig == null) { - throw new IllegalArgumentException(); - } - if (memoryStore == null) { - memoryStore = new VolatileMemoryStore(); - } - - return new DefaultKernel(kernelConfig, promptTemplateEngine, memoryStore); - } -} diff --git a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/VolatileMemoryStore.java b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/VolatileMemoryStore.java index eaec6b4a23..29e04bfad1 100644 --- a/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/VolatileMemoryStore.java +++ b/java/semantickernel-core/src/main/java/com/microsoft/semantickernel/memory/VolatileMemoryStore.java @@ -98,8 +98,8 @@ public class VolatileMemoryStore implements MemoryStore { @Override public Mono getAsync( @Nonnull String collectionName, @Nonnull String key, boolean withEmbedding) { - Map collection = getCollection(collectionName); - MemoryRecord record = collection.get(key); + Map collection = this._store.get(collectionName); + MemoryRecord record = collection != null ? collection.get(key) : null; if (record != null) { if (withEmbedding) { return Mono.just(record); @@ -143,16 +143,16 @@ public class VolatileMemoryStore implements MemoryStore { @Override public Mono removeAsync(@Nonnull String collectionName, @Nonnull String key) { - Map collection = getCollection(collectionName); - collection.remove(key); + Map collection = this._store.get(collectionName); + if (collection != null) collection.remove(key); return Mono.empty(); } @Override public Mono removeBatchAsync( @Nonnull String collectionName, @Nonnull Collection keys) { - Map collection = getCollection(collectionName); - keys.forEach(key -> collection.remove(key)); + Map collection = this._store.get(collectionName); + keys.forEach(collection::remove); return Mono.empty(); } @@ -220,7 +220,7 @@ public class VolatileMemoryStore implements MemoryStore { @Override public Mono> getNearestMatchAsync( @Nonnull String collectionName, - @Nonnull Embedding embedding, + @Nonnull Embedding embedding, double minRelevanceScore, boolean withEmbedding) { diff --git a/java/semantickernel-core/src/main/resources/META-INF/services/com.microsoft.semantickernel.Kernel.InternalBuilder b/java/semantickernel-core/src/main/resources/META-INF/services/com.microsoft.semantickernel.Kernel$InternalBuilder similarity index 100% rename from java/semantickernel-core/src/main/resources/META-INF/services/com.microsoft.semantickernel.Kernel.InternalBuilder rename to java/semantickernel-core/src/main/resources/META-INF/services/com.microsoft.semantickernel.Kernel$InternalBuilder diff --git a/java/semantickernel-core/src/main/resources/META-INF/services/com.microsoft.semantickernel.textcompletion.TextCompletion$Builder b/java/semantickernel-core/src/main/resources/META-INF/services/com.microsoft.semantickernel.textcompletion.TextCompletion$Builder new file mode 100644 index 0000000000..39bdf4b287 --- /dev/null +++ b/java/semantickernel-core/src/main/resources/META-INF/services/com.microsoft.semantickernel.textcompletion.TextCompletion$Builder @@ -0,0 +1 @@ +com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion$Builder \ No newline at end of file diff --git a/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatCompletionTest.java b/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatCompletionTest.java new file mode 100644 index 0000000000..226140a2d5 --- /dev/null +++ b/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/connectors/ai/openai/chatcompletion/OpenAIChatCompletionTest.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.connectors.ai.openai.chatcompletion; + +import static com.microsoft.semantickernel.DefaultKernelTest.mockCompletionOpenAIAsyncClient; + +import com.azure.ai.openai.models.ChatCompletions; +import com.microsoft.openai.AzureOpenAIClient; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.KernelConfig; +import com.microsoft.semantickernel.ai.AIException; +import com.microsoft.semantickernel.builders.SKBuilders; +import com.microsoft.semantickernel.chatcompletion.ChatCompletion; +import com.microsoft.semantickernel.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.syntaxexamples.Example17ChatGPTTest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; + +import java.util.ArrayList; + +public class OpenAIChatCompletionTest { + @Test + public void azureOpenAIChatSampleAsync() { + AzureOpenAIClient client = + mockCompletionOpenAIAsyncClient(Tuples.of("Today is", "A-RESULT")); + + String message = "Hi, I'm looking for book suggestions"; + Example17ChatGPTTest.mockResponse(client, message, "1st response"); + + String result = getRunExample(client, message); + Assertions.assertEquals("1st response", result); + } + + private static String getRunExample(AzureOpenAIClient client, String message) { + KernelConfig kernelConfig = + SKBuilders.kernelConfig() + .addChatCompletionService( + "gpt-3.5-turbo-0301", + kernel -> + SKBuilders.chatCompletion() + .build(client, "gpt-3.5-turbo-0301")) + .build(); + + Kernel kernel = SKBuilders.kernel().setKernelConfig(kernelConfig).build(); + + ChatCompletion chatGPT = kernel.getService(null, ChatCompletion.class); + + OpenAIChatHistory chatHistory = + chatGPT.createNewChat("You are a librarian, expert about books"); + + // First user message + chatHistory.addUserMessage(message); + ChatHistory.Message createdMessage = chatHistory.getLastMessage().get(); + Assertions.assertEquals(ChatHistory.AuthorRoles.User, createdMessage.getAuthorRoles()); + Assertions.assertEquals(message, createdMessage.getContent()); + + // First bot assistant message + return chatGPT.generateMessageAsync(chatHistory, null).block(); + } + + private static class runExample { + public final OpenAIChatHistory chatHistory; + public final String reply; + + public runExample(OpenAIChatHistory chatHistory, String reply) { + this.chatHistory = chatHistory; + this.reply = reply; + } + } + + @Test + public void emptyResponseThrowsError() { + AzureOpenAIClient client = + mockCompletionOpenAIAsyncClient(Tuples.of("Today is", "A-RESULT")); + + String message = "Hi, I'm looking for book suggestions"; + + ChatCompletions completion = Mockito.mock(ChatCompletions.class); + Mockito.when(completion.getChoices()).thenReturn(new ArrayList<>()); + + Mockito.when( + client.getChatCompletions( + Mockito.any(), + Mockito.argThat( + msg -> { + return msg != null + && msg.getMessages() + .get(msg.getMessages().size() - 1) + .getContent() + .equals(message); + }))) + .thenReturn(Mono.just(completion)); + + Assertions.assertThrows(AIException.class, () -> getRunExample(client, message)); + } +} diff --git a/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/memory/VolatileMemoryStoreTests.java b/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/memory/VolatileMemoryStoreTests.java index e6b91bd12b..ce583d99fc 100644 --- a/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/memory/VolatileMemoryStoreTests.java +++ b/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/memory/VolatileMemoryStoreTests.java @@ -8,11 +8,15 @@ import com.microsoft.semantickernel.ai.embeddings.Embedding; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; + import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; class VolatileMemoryStoreTests { private VolatileMemoryStore _db; @@ -123,7 +127,7 @@ class VolatileMemoryStoreTests { } @Test - void GetAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() { + void getAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() { // Arrange MemoryRecord testRecord = MemoryRecord.localRecord( @@ -146,469 +150,564 @@ class VolatileMemoryStoreTests { // Assert assertNotNull(actualDefault); assertNotNull(actualDefault.getEmbedding()); + assertNotNull(actualDefault.getEmbedding().getVector()); assertTrue(actualDefault.getEmbedding().getVector().isEmpty()); assertNotNull(actualWithEmbedding); assertNotNull(actualWithEmbedding.getEmbedding()); + assertNotNull(actualWithEmbedding.getEmbedding().getVector()); assertFalse(actualWithEmbedding.getEmbedding().getVector().isEmpty()); assertNotEquals(testRecord, actualDefault); assertEquals(testRecord, actualWithEmbedding); } - /* - @Test - void itCanUpsertAndRetrieveARecordWithNoTimestampAsync() - { - // Arrange - MemoryRecord testRecord = MemoryRecord.LocalRecord( - id: "test", - text: "text", - description: "description", - embedding: new Embedding(new float[] { 1, 2, 3 }), - key: null, - timestamp: null); - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - // Act - await this._db.CreateCollectionAsync(collection); - var key = await this._db.UpsertAsync(collection, testRecord); - var actual = await this._db.GetAsync(collection, key, true); + @Test + void itCanUpsertAndRetrieveARecordWithNoTimestampAsync() { + // Arrange + MemoryRecord testRecord = + MemoryRecord.localRecord( + "test", + "text", + "description", + new Embedding(Arrays.asList(1f, 2f, 3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; - // Assert - Assert.NotNull(actual); - Assert.Equal(testRecord, actual); + // Act + this._db.createCollectionAsync(collection).block(); + String key = this._db.upsertAsync(collection, testRecord).block(); + MemoryRecord actual = this._db.getAsync(collection, key, true).block(); + + // Assert + assertNotNull(actual); + assertEquals(testRecord, actual); + } + + @Test + void itCanUpsertAndRetrieveARecordWithTimestampAsync() { + // Arrange + MemoryRecord testRecord = + MemoryRecord.localRecord( + "test", + "text", + "description", + new Embedding(Arrays.asList(1f, 2f, 3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + ZonedDateTime.now()); + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + + // Act + this._db.createCollectionAsync(collection).block(); + String key = this._db.upsertAsync(collection, testRecord).block(); + MemoryRecord actual = this._db.getAsync(collection, key, true).block(); + + // Assert + assertNotNull(actual); + assertEquals(testRecord, actual); + } + + @Test + void UpsertReplacesExistingRecordWithSameIdAsync() { + // Arrange + String commonId = "test"; + MemoryRecord testRecord = + MemoryRecord.localRecord( + commonId, + "text", + "description", + new Embedding(Arrays.asList(1f, 2f, 3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + MemoryRecord testRecord2 = + MemoryRecord.localRecord( + commonId, + "text2", + "description2", + new Embedding(Arrays.asList(1f, 2f, 4f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + + // Act + this._db.createCollectionAsync(collection).block(); + String key = this._db.upsertAsync(collection, testRecord).block(); + String key2 = this._db.upsertAsync(collection, testRecord2).block(); + MemoryRecord actual = this._db.getAsync(collection, key, true).block(); + + // Assert + assertNotNull(actual); + assertNotEquals(testRecord, actual); + assertEquals(key, key2); + assertEquals(testRecord2, actual); + } + + @Test + void ExistingRecordCanBeRemovedAsync() { + // Arrange + MemoryRecord testRecord = + MemoryRecord.localRecord( + "test", + "text", + "description", + new Embedding(Arrays.asList(1f, 2f, 3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + + // Act + this._db.createCollectionAsync(collection).block(); + String key = this._db.upsertAsync(collection, testRecord).block(); + assertNotNull(key); + this._db.removeAsync(collection, key).block(); + MemoryRecord actual = this._db.getAsync(collection, key, false).block(); + + // Assert + assertNull(actual); + } + + @Test + void RemovingNonExistingRecordDoesNothingAsync() { + // Arrange + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + + // Act + this._db.removeAsync(collection, "key").block(); + MemoryRecord actual = this._db.getAsync(collection, "key", false).block(); + + // Assert + assertNull(actual); + } + + @Test + void itCanListAllDatabaseCollectionsAsync() { + // Arrange + String[] testCollections = {"test_collection5", "test_collection6", "test_collection7"}; + this._collectionNum += 3; + this._db.createCollectionAsync(testCollections[0]).block(); + this._db.createCollectionAsync(testCollections[1]).block(); + this._db.createCollectionAsync(testCollections[2]).block(); + + // Act + Collection collections = this._db.getCollectionsAsync().block(); + + // Assert + assertNotNull(collections); + assertEquals(3, collections.size()); + assertTrue( + collections.contains(testCollections[0]), + "Collections does not contain the newly-created collection " + testCollections[0]); + assertTrue( + collections.contains(testCollections[1]), + "Collections does not contain the newly-created collection " + testCollections[1]); + assertTrue( + collections.contains(testCollections[2]), + "Collections does not contain the newly-created collection " + testCollections[2]); + } + + @Test + void GetNearestMatchesReturnsAllResultsWithNoMinScoreAsync() { + // Arrange + Embedding compareEmbedding = new Embedding<>(Arrays.asList(1f, 1f, 1f)); + int topN = 4; + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + this._db.createCollectionAsync(collection).block(); + int i = 0; + MemoryRecord testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, 1f, 1f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(-1f, -1f, -1f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, 2f, 3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(-1f, -2f, -3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, -1f, -2f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + // Act + double threshold = -1; + Collection> topNResults = + this._db + .getNearestMatchesAsync( + collection, compareEmbedding, topN, threshold, false) + .block(); + + // Assert + assertNotNull(topNResults); + assertEquals(topN, topNResults.size()); + Tuple2[] topNResultsArray = topNResults.toArray(new Tuple2[0]); + for (int j = 0; j < topN - 1; j++) { + int compare = + Double.compare( + topNResultsArray[j].getT2().doubleValue(), + topNResultsArray[j + 1].getT2().doubleValue()); + assertTrue(compare >= 0); + } + } + + @Test + void GetNearestMatchAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() { + // Arrange + Embedding compareEmbedding = new Embedding<>(Arrays.asList(1f, 1f, 1f)); + int topN = 4; + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + this._db.createCollectionAsync(collection).block(); + int i = 0; + MemoryRecord testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, 1f, 1f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(-1f, -1f, -1f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, 2f, 3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(-1f, -2f, -3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, -1f, -2f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + // Act + double threshold = 0.75; + Tuple2 topNResultDefault = + this._db + .getNearestMatchAsync(collection, compareEmbedding, threshold, false) + .block(); + Tuple2 topNResultWithEmbedding = + this._db + .getNearestMatchAsync(collection, compareEmbedding, threshold, true) + .block(); + + // Assert + assertNotNull(topNResultDefault); + assertNotNull(topNResultWithEmbedding); + assertNotNull(topNResultDefault.getT1().getEmbedding()); + assertNotNull(topNResultDefault.getT1().getEmbedding().getVector()); + assertTrue(topNResultDefault.getT1().getEmbedding().getVector().isEmpty()); + assertNotNull(topNResultWithEmbedding.getT1().getEmbedding()); + assertNotNull(topNResultWithEmbedding.getT1().getEmbedding().getVector()); + assertFalse(topNResultWithEmbedding.getT1().getEmbedding().getVector().isEmpty()); + } + + @Test + void GetNearestMatchAsyncReturnsExpectedAsync() { + // Arrange + Embedding compareEmbedding = new Embedding<>(Arrays.asList(1f, 1f, 1f)); + int topN = 4; + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + this._db.createCollectionAsync(collection).block(); + int i = 0; + MemoryRecord testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, 1f, 1f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(-1f, -1f, -1f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, 2f, 3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(-1f, -2f, -3f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + i++; + testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding(Arrays.asList(1f, -1f, -2f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); + + // Act + double threshold = 0.75; + Tuple2 topNResult = + this._db + .getNearestMatchAsync(collection, compareEmbedding, threshold, false) + .block(); + + // Assert + assertNotNull(topNResult); + assertEquals("test0", topNResult.getT1().getMetadata().getId()); + assertTrue(topNResult.getT2().doubleValue() >= threshold); + } + + @Test + void getNearestMatchesDifferentiatesIdenticalVectorsByKeyAsync() { + // Arrange + Embedding compareEmbedding = new Embedding<>(Arrays.asList(1f, 1f, 1f)); + int topN = 4; + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + this._db.createCollectionAsync(collection).block(); + + for (int i = 0; i < 10; i++) { + MemoryRecord testRecord = + MemoryRecord.localRecord( + "test" + i, + "text" + i, + "description" + i, + new Embedding<>(Arrays.asList(1f, 1f, 1f)), + NULL_ADDITIONAL_METADATA, + NULL_KEY, + NULL_TIMESTAMP); + this._db.upsertAsync(collection, testRecord).block(); } - @Test - void itCanUpsertAndRetrieveARecordWithTimestampAsync() - { - // Arrange - MemoryRecord testRecord = MemoryRecord.LocalRecord( - id: "test", - text: "text", - description: "description", - embedding: new Embedding(new float[] { 1, 2, 3 }), - key: null, - timestamp: DateTimeOffset.UtcNow); - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; + // Act + Collection> topNResults = + this._db + .getNearestMatchesAsync(collection, compareEmbedding, topN, 0.75, true) + .block(); + Collection topNKeys = + topNResults.stream() + .map(tuple -> tuple.getT1().getKey()) + .sorted() + .collect(Collectors.toList()); - // Act - await this._db.CreateCollectionAsync(collection); - var key = await this._db.UpsertAsync(collection, testRecord); - var actual = await this._db.GetAsync(collection, key, true); + // Assert + assertEquals(topN, topNResults.size()); + assertEquals(topN, topNKeys.size()); + for (Iterator> iterator = topNResults.iterator(); + iterator.hasNext(); ) { + Tuple2 tuple = iterator.next(); + int compare = Double.compare(tuple.getT2().doubleValue(), 0.75); + assertTrue(topNKeys.contains(tuple.getT1().getKey())); + assertTrue(compare >= 0); + } + } - // Assert - Assert.NotNull(actual); - Assert.Equal(testRecord, actual); + @Test + void itCanBatchUpsertRecordsAsync() { + // Arrange + int numRecords = 10; + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + this._db.createCollectionAsync(collection).block(); + Collection records = this.createBatchRecords(numRecords); + + // Act + Collection keys = this._db.upsertBatchAsync(collection, records).block(); + Collection resultRecords = + this._db.getBatchAsync(collection, keys, false).block(); + + // Assert + assertNotNull(keys); + assertEquals(numRecords, keys.size()); + assertEquals(numRecords, resultRecords.size()); + } + + @Test + void itCanBatchGetRecordsAsync() { + // Arrange + int numRecords = 10; + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + this._db.createCollectionAsync(collection).block(); + Collection records = this.createBatchRecords(numRecords); + Collection keys = this._db.upsertBatchAsync(collection, records).block(); + + // Act + Collection results = this._db.getBatchAsync(collection, keys, false).block(); + + // Assert + assertNotNull(keys); + assertNotNull(results); + assertEquals(numRecords, results.size()); + } + + @Test + void itCanBatchRemoveRecordsAsync() { + // Arrange + int numRecords = 10; + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; + this._db.createCollectionAsync(collection).block(); + Collection records = this.createBatchRecords(numRecords); + + List keys = new ArrayList<>(); + for (String key : this._db.upsertBatchAsync(collection, records).block()) { + keys.add(key); } - @Test - void UpsertReplacesExistingRecordWithSameIdAsync() - { - // Arrange - String commonId = "test"; - MemoryRecord testRecord = MemoryRecord.LocalRecord( - id: commonId, - text: "text", - description: "description", - embedding: new Embedding(new float[] { 1, 2, 3 })); - MemoryRecord testRecord2 = MemoryRecord.LocalRecord( - id: commonId, - text: "text2", - description: "description2", - embedding: new Embedding(new float[] { 1, 2, 4 })); - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; + // Act + this._db.removeBatchAsync(collection, keys); - // Act - await this._db.CreateCollectionAsync(collection); - var key = await this._db.UpsertAsync(collection, testRecord); - var key2 = await this._db.UpsertAsync(collection, testRecord2); - var actual = await this._db.GetAsync(collection, key, true); + // Assert + for (MemoryRecord result : this._db.getBatchAsync(collection, keys, false).block()) { + assertNull(result); + } + } - // Assert - Assert.NotNull(actual); - Assert.NotEqual(testRecord, actual); - Assert.Equal(key, key2); - Assert.Equal(testRecord2, actual); + @Test + void CollectionsCanBeDeletedAsync() { + // Arrange + Collection collections = this._db.getCollectionsAsync().block(); + int numCollections = collections.size(); + assertEquals(this._collectionNum, numCollections); + + // Act + for (String collection : collections) { + this._db.deleteCollectionAsync(collection); } - @Test - void ExistingRecordCanBeRemovedAsync() - { - // Arrange - MemoryRecord testRecord = MemoryRecord.LocalRecord( - id: "test", - text: "text", - description: "description", - embedding: new Embedding(new float[] { 1, 2, 3 })); - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; + // Assert + collections = this._db.getCollectionsAsync().block(); + numCollections = collections.size(); + assertEquals(0, numCollections); + this._collectionNum = 0; + } - // Act - await this._db.CreateCollectionAsync(collection); - var key = await this._db.UpsertAsync(collection, testRecord); - await this._db.RemoveAsync(collection, key); - var actual = await this._db.GetAsync(collection, key); + @Test + void itThrowsWhenDeletingNonExistentCollectionAsync() { + // Arrange + String collection = "test_collection" + this._collectionNum; + this._collectionNum++; - // Assert - Assert.Null(actual); - } - - @Test - void RemovingNonExistingRecordDoesNothingAsync() - { - // Arrange - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - - // Act - await this._db.RemoveAsync(collection, "key"); - var actual = await this._db.GetAsync(collection, "key"); - - // Assert - Assert.Null(actual); - } - - @Test - void itCanListAllDatabaseCollectionsAsync() - { - // Arrange - String[] testCollections = { "test_collection5", "test_collection6", "test_collection7" }; - this._collectionNum += 3; - await this._db.CreateCollectionAsync(testCollections[0]); - await this._db.CreateCollectionAsync(testCollections[1]); - await this._db.CreateCollectionAsync(testCollections[2]); - - // Act - var collections = this._db.GetCollectionsAsync().ToEnumerable(); - - #pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection - // Assert - Assert.NotNull(collections); - assertTrue(collections.Any(), "Collections is empty"); - Assert.Equal(3, collections.Count()); - assertTrue(collections.Contains(testCollections[0]), - $"Collections does not contain the newly-created collection {testCollections[0]}"); - assertTrue(collections.Contains(testCollections[1]), - $"Collections does not contain the newly-created collection {testCollections[1]}"); - assertTrue(collections.Contains(testCollections[2]), - $"Collections does not contain the newly-created collection {testCollections[2]}"); - } - #pragma warning restore CA1851 // Possible multiple enumerations of 'IEnumerable' collection - - @Test - void GetNearestMatchesReturnsAllResultsWithNoMinScoreAsync() - { - // Arrange - var compareEmbedding = new Embedding(new float[] { 1, 1, 1 }); - int topN = 4; - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - await this._db.CreateCollectionAsync(collection); - int i = 0; - MemoryRecord testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, 1, 1 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { -1, -1, -1 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, 2, 3 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { -1, -2, -3 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, -1, -2 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - // Act - double threshold = -1; - var topNResults = this._db.GetNearestMatchesAsync(collection, compareEmbedding, limit: topN, minRelevanceScore: threshold).ToEnumerable().ToArray(); - - // Assert - Assert.Equal(topN, topNResults.Length); - for (int j = 0; j < topN - 1; j++) - { - int compare = topNResults[j].Item2.CompareTo(topNResults[j + 1].Item2); - assertTrue(compare >= 0); - } - } - - @Test - void GetNearestMatchAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() - { - // Arrange - var compareEmbedding = new Embedding(new float[] { 1, 1, 1 }); - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - await this._db.CreateCollectionAsync(collection); - int i = 0; - MemoryRecord testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, 1, 1 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { -1, -1, -1 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, 2, 3 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { -1, -2, -3 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, -1, -2 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - // Act - double threshold = 0.75; - var topNResultDefault = await this._db.GetNearestMatchAsync(collection, compareEmbedding, minRelevanceScore: threshold); - var topNResultWithEmbedding = await this._db.GetNearestMatchAsync(collection, compareEmbedding, minRelevanceScore: threshold, withEmbedding: true); - - // Assert - Assert.NotNull(topNResultDefault); - Assert.NotNull(topNResultWithEmbedding); - Assert.Empty(topNResultDefault.Value.Item1.Embedding.Vector); - Assert.NotEmpty(topNResultWithEmbedding.Value.Item1.Embedding.Vector); - } - - @Test - void GetNearestMatchAsyncReturnsExpectedAsync() - { - // Arrange - var compareEmbedding = new Embedding(new float[] { 1, 1, 1 }); - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - await this._db.CreateCollectionAsync(collection); - int i = 0; - MemoryRecord testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, 1, 1 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { -1, -1, -1 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, 2, 3 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { -1, -2, -3 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - i++; - testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, -1, -2 })); - _ = await this._db.UpsertAsync(collection, testRecord); - - // Act - double threshold = 0.75; - var topNResult = await this._db.GetNearestMatchAsync(collection, compareEmbedding, minRelevanceScore: threshold); - - // Assert - Assert.NotNull(topNResult); - Assert.Equal("test0", topNResult.Value.Item1.Metadata.Id); - assertTrue(topNResult.Value.Item2 >= threshold); - } - - @Test - void GetNearestMatchesDifferentiatesIdenticalVectorsByKeyAsync() - { - // Arrange - var compareEmbedding = new Embedding(new float[] { 1, 1, 1 }); - int topN = 4; - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - await this._db.CreateCollectionAsync(collection); - - for (int i = 0; i < 10; i++) - { - MemoryRecord testRecord = MemoryRecord.LocalRecord( - id: "test" + i, - text: "text" + i, - description: "description" + i, - embedding: new Embedding(new float[] { 1, 1, 1 })); - _ = await this._db.UpsertAsync(collection, testRecord); - } - - // Act - var topNResults = this._db.GetNearestMatchesAsync(collection, compareEmbedding, limit: topN, minRelevanceScore: 0.75).ToEnumerable().ToArray(); - IEnumerable topNKeys = topNResults.Select(x => x.Item1.Key).ToImmutableSortedSet(); - - // Assert - Assert.Equal(topN, topNResults.Length); - Assert.Equal(topN, topNKeys.Count()); - - for (int i = 0; i < topNResults.Length; i++) - { - int compare = topNResults[i].Item2.CompareTo(0.75); - assertTrue(compare >= 0); - } - } - - @Test - void itCanBatchUpsertRecordsAsync() - { - // Arrange - int numRecords = 10; - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - await this._db.CreateCollectionAsync(collection); - IEnumerable records = this.CreateBatchRecords(numRecords); - - // Act - var keys = this._db.UpsertBatchAsync(collection, records); - var resultRecords = this._db.GetBatchAsync(collection, keys.ToEnumerable()); - - // Assert - Assert.NotNull(keys); - Assert.Equal(numRecords, keys.ToEnumerable().Count()); - Assert.Equal(numRecords, resultRecords.ToEnumerable().Count()); - } - - @Test - void itCanBatchGetRecordsAsync() - { - // Arrange - int numRecords = 10; - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - await this._db.CreateCollectionAsync(collection); - IEnumerable records = this.CreateBatchRecords(numRecords); - var keys = this._db.UpsertBatchAsync(collection, records); - - // Act - var results = this._db.GetBatchAsync(collection, keys.ToEnumerable()); - - // Assert - Assert.NotNull(keys); - Assert.NotNull(results); - Assert.Equal(numRecords, results.ToEnumerable().Count()); - } - - @Test - void itCanBatchRemoveRecordsAsync() - { - // Arrange - int numRecords = 10; - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - await this._db.CreateCollectionAsync(collection); - IEnumerable records = this.CreateBatchRecords(numRecords); - - List keys = new List(); - await foreach (var key in this._db.UpsertBatchAsync(collection, records)) - { - keys.Add(key); - } - - // Act - await this._db.RemoveBatchAsync(collection, keys); - - // Assert - await foreach (var result in this._db.GetBatchAsync(collection, keys)) - { - assertNull(result); - } - } - - @Test - void CollectionsCanBeDeletedAsync() - { - // Arrange - var collections = this._db.GetCollectionsAsync().ToEnumerable(); - int numCollections = collections.Count(); - assertTrue(numCollections == this._collectionNum); - - // Act - foreach (var collection in collections) - { - await this._db.DeleteCollectionAsync(collection); - } - - // Assert - collections = this._db.GetCollectionsAsync().ToEnumerable(); - numCollections = collections.Count(); - assertTrue(numCollections == 0); - this._collectionNum = 0; - } - - @Test - void itThrowsWhenDeletingNonExistentCollectionAsync() - { - // Arrange - String collection = "test_collection" + this._collectionNum; - this._collectionNum++; - - // Act - await Assert.ThrowsAsync(() => this._db.DeleteCollectionAsync(collection)); - } - - */ + // Act + assertThrows( + MemoryException.class, () -> this._db.deleteCollectionAsync(collection).block()); + } } diff --git a/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/syntaxexamples/Example17ChatGPTTest.java b/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/syntaxexamples/Example17ChatGPTTest.java new file mode 100644 index 0000000000..da3241a5fb --- /dev/null +++ b/java/semantickernel-core/src/test/java/com/microsoft/semantickernel/syntaxexamples/Example17ChatGPTTest.java @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.syntaxexamples; + +import static com.microsoft.semantickernel.DefaultKernelTest.mockCompletionOpenAIAsyncClient; + +import com.azure.ai.openai.models.ChatChoice; +import com.azure.ai.openai.models.ChatCompletions; +import com.azure.ai.openai.models.ChatMessage; +import com.microsoft.openai.AzureOpenAIClient; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.KernelConfig; +import com.microsoft.semantickernel.builders.SKBuilders; +import com.microsoft.semantickernel.chatcompletion.ChatCompletion; +import com.microsoft.semantickernel.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatHistory; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; + +import java.util.Arrays; + +public class Example17ChatGPTTest { + + @Test + public void azureOpenAIChatSampleAsync() { + + AzureOpenAIClient client = + mockCompletionOpenAIAsyncClient(Tuples.of("Today is", "A-RESULT")); + + String message = "Hi, I'm looking for book suggestions"; + String secondMessage = + "I love history and philosophy, I'd like to learn something new about Greece, any" + + " suggestion?"; + + mockResponse(client, message, "1st response"); + mockResponse(client, secondMessage, "2nd response"); + + KernelConfig kernelConfig = + SKBuilders.kernelConfig() + .addChatCompletionService( + "gpt-3.5-turbo-0301", + kernel -> + SKBuilders.chatCompletion() + .build(client, "gpt-3.5-turbo-0301")) + .build(); + + Kernel kernel = SKBuilders.kernel().setKernelConfig(kernelConfig).build(); + + ChatCompletion chatGPT = kernel.getService(null, ChatCompletion.class); + + OpenAIChatHistory chatHistory = + chatGPT.createNewChat("You are a librarian, expert about books"); + + // First user message + chatHistory.addUserMessage(message); + ChatHistory.Message createdMessage = + chatHistory.getMessages().get(chatHistory.getMessages().size() - 1); + Assertions.assertEquals(ChatHistory.AuthorRoles.User, createdMessage.getAuthorRoles()); + Assertions.assertEquals(message, createdMessage.getContent()); + + // First bot assistant message + String reply = chatGPT.generateMessageAsync(chatHistory, null).block(); + Assertions.assertEquals("1st response", reply); + chatHistory.addAssistantMessage(reply); + + // Second user message + chatHistory.addUserMessage(secondMessage); + + // Second bot assistant message + reply = chatGPT.generateMessageAsync(chatHistory, null).block(); + Assertions.assertEquals("2nd response", reply); + } + + public static void mockResponse(AzureOpenAIClient client, String message, String response) { + ChatMessage chatMessage = Mockito.mock(ChatMessage.class); + ChatChoice chatChoice = Mockito.mock(ChatChoice.class); + ChatCompletions completion = Mockito.mock(ChatCompletions.class); + + Mockito.when(completion.getChoices()).thenReturn(Arrays.asList(chatChoice)); + + Mockito.when(chatChoice.getMessage()).thenReturn(chatMessage); + + Mockito.when(chatMessage.getContent()).thenReturn(response); + + Mockito.when( + client.getChatCompletions( + Mockito.any(), + Mockito.argThat( + msg -> { + return msg != null + && msg.getMessages() + .get(msg.getMessages().size() - 1) + .getContent() + .equals(message); + }))) + .thenReturn(Mono.just(completion)); + } +}