diff --git a/.github/_typos.toml b/.github/_typos.toml
index a92576ba3..c101713b6 100644
--- a/.github/_typos.toml
+++ b/.github/_typos.toml
@@ -29,6 +29,7 @@ ans = "ans" # Short for answers
arange = "arange" # Method in Python numpy package
prompty = "prompty" # prompty is a format name.
ist = "ist" # German language
+Prelease = "Prelease" # Prelease is a format name.
[default.extend-identifiers]
ags = "ags" # Azure Graph Service
diff --git a/.github/workflows/close-inactive-issues.yml b/.github/workflows/close-inactive-issues.yml
index 10244c760..bbd6a87c2 100644
--- a/.github/workflows/close-inactive-issues.yml
+++ b/.github/workflows/close-inactive-issues.yml
@@ -10,7 +10,7 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@v5
+ - uses: actions/stale@v11
with:
days-before-issue-stale: 90
days-before-issue-close: 14
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index caf569803..5fe9e853c 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -30,11 +30,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -48,10 +48,10 @@ jobs:
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
if: ${{ matrix.language != 'java' }}
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@v4
- name: Setup JDK
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
if: ${{ matrix.language == 'java' }}
with:
java-version: 17
@@ -73,6 +73,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/java-build.yml b/.github/workflows/java-build.yml
index d4b487cfc..bf35695ae 100644
--- a/.github/workflows/java-build.yml
+++ b/.github/workflows/java-build.yml
@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
# Need to use JDK 11 to build for JDK 8
- name: Set JDK
@@ -41,7 +41,7 @@ jobs:
fi
# Sets up the specified JDK version from the matrix
- - uses: actions/setup-java@v4
+ - uses: actions/setup-java@v5
with:
java-version: ${{ steps.set-jdk.outputs.JDK_VERSION }}
distribution: microsoft
@@ -56,7 +56,7 @@ jobs:
run: ./mvnw -B -Pbug-check -Pcompile-jdk${{ matrix.java-versions }} test --file pom.xml
# Uploads test artifacts for each JDK version
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v7
if: always()
with:
name: test_output_sk_jdk${{ matrix.java-versions }}u
diff --git a/.github/workflows/java-integration-tests.yml b/.github/workflows/java-integration-tests.yml
index a65ee5ccd..45afb219e 100644
--- a/.github/workflows/java-integration-tests.yml
+++ b/.github/workflows/java-integration-tests.yml
@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
# Need to use JDK 11 to build for JDK 8
- name: Set JDK
@@ -36,7 +36,7 @@ jobs:
echo "JDK_VERSION=$version" >> $GITHUB_OUTPUT
fi
- - uses: actions/setup-java@v4
+ - uses: actions/setup-java@v5
with:
java-version: ${{ steps.set-jdk.outputs.JDK_VERSION }}
distribution: microsoft
diff --git a/.github/workflows/java-publish-package.yml b/.github/workflows/java-publish-package.yml
index 25afec461..38512904f 100644
--- a/.github/workflows/java-publish-package.yml
+++ b/.github/workflows/java-publish-package.yml
@@ -17,10 +17,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
# Sets up the specified JDK version from the matrix
- - uses: actions/setup-java@v4
+ - uses: actions/setup-java@v5
with:
java-version: 11
distribution: microsoft
@@ -30,7 +30,7 @@ jobs:
run: ./mvnw -B -DskipTests -Pcompile-jdk8 -P-compile-jdk17 clean deploy --file pom.xml -DaltDeploymentRepository=local::file:///tmp/target/staging-deploy
- name: Upload Artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: Artifacts
path: /tmp/target/staging-deploy
diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml
index 428061881..d5a48785b 100644
--- a/.github/workflows/label-issues.yml
+++ b/.github/workflows/label-issues.yml
@@ -13,7 +13,7 @@ jobs:
permissions:
issues: write
steps:
- - uses: actions/github-script@v7
+ - uses: actions/github-script@v9
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
diff --git a/.github/workflows/label-title-prefix.yml b/.github/workflows/label-title-prefix.yml
index f443c9809..e84551dda 100644
--- a/.github/workflows/label-title-prefix.yml
+++ b/.github/workflows/label-title-prefix.yml
@@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- - uses: actions/github-script@v7
+ - uses: actions/github-script@v9
name: "Issue/PR: update title"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml
index c09936256..530cb5f48 100644
--- a/.github/workflows/markdown-link-check.yml
+++ b/.github/workflows/markdown-link-check.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
# check out the latest version of the code
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
# Checks the status of hyperlinks in .md files in verbose mode
- name: Check links
diff --git a/.github/workflows/typos.yaml b/.github/workflows/typos.yaml
index d7a937bad..c6845f2ab 100644
--- a/.github/workflows/typos.yaml
+++ b/.github/workflows/typos.yaml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Use custom config file
uses: crate-ci/typos@master
diff --git a/.gitignore b/.gitignore
index e6f26223b..44c24fc06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,3 +61,4 @@ __pycache__/
__pypackages__/
.pdm.toml
global.json
+.java-version
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 6d3a56651..d58dfb70b 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -14,5 +14,6 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 16667e414..6f3bd17c1 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -21,5 +21,6 @@
},
},
"java.debug.settings.onBuildFailureProceed": true,
- "java.compile.nullAnalysis.mode": "disabled"
+ "java.compile.nullAnalysis.mode": "disabled",
+ "java.configuration.updateBuildConfiguration": "interactive"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8942e4974..0267d08f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,59 @@
+# 1.4.4-RC2
+
+- Upgrade many dependencies to recent versions
+- Migrate from "Tool calls" to "Function calls" terminology, deprecated OpenAIFunctionToolCall
+- Refactored Data storage area to be more extensible and added Oracle Database support (Thank you to the contributors
+ from Oracle for the contribution)
+
+# 1.4.4-RC1
+
+- Add Agent framework abstractions.
+- Add ChatCompletionAgent implementation.
+- Add FunctionChoiceBehavior for OpenAI, replacing the older ToolCallBehavior.
+
+# 1.4.3
+
+- Bug fix for execution on Android (https://github.com/microsoft/semantic-kernel-java/pull/284)
+- Upgrade to azure-ai-openai 1.0.0-beta.14
+
+# 1.4.2
+
+- Fix bug effecting using native Java methods with OpenAI tool calling
+
+# 1.4.1
+
+- Add Otel Telemetry on function invocations
+- Fix bug to add type information to OpenAI function parameters
+- Improve efficiency of cosine similarity calculation
+- Fix concurrency bugs on database creation
+- Add sample demonstrating a text splitter for chunking text for embedding
+- Add hybridSearchAsync support to Azure AI Search
+
+# 1.4.0
+
+- Upgrade to azure-ai-openai 1.0.0-beta.12
+- Add vector stores with vector search support for Azure AI Search, Redis, JDBC with Postgres, MySQL, SQLite and HSQLDB.
+ Moving these features out of the experimental stage.
+
+# 1.3.0
+
+- Added support for Json Schema to Open AI Chat Completions
+- Upgraded to openai sdk 1.0.0-beta.11
+- Added convenience method `FunctionInvocation.withResultTypeAutoConversion` which sets the return type and registers a
+ type converter based on Jackson for the return type.
+- Added localization support for error/debug messages
+- Add vector search to experimental vector stores.
+ - Approximate vector search for Azure AI Search, Redis and JDBC with Postgres.
+ - Exhaustive vector search for VolatileVectorStore and default JDBC query provider, MySQL, SQLite and HSQLDB.
+
+### Bug Fixes
+
+- Fixed type converters not being passed on to be used in tool invocations
+
+### Breaking Changes
+
+- To support the new Json Schema feature, ResponseFormat has changed from an enum to a class.
+
# 1.2.2
- Fix bug in `FunctionInvocation` not using per-invocation type conversion when calling `withResultType`.
diff --git a/COMMUNITY.md b/COMMUNITY.md
index be98d4253..7afdb02cb 100644
--- a/COMMUNITY.md
+++ b/COMMUNITY.md
@@ -7,22 +7,6 @@ Below are some ways that you can get involved in the SK Community.
File issues, submit PRs, and provide feedback and ideas to what you'd like to see from the Semantic Kernel.
We do our best to respond to each submission.
-## Public Community Office Hours
-
-We regularly have Community Office Hours that are open to the **public** to join.
-
-Add Semantic Kernel events to your calendar - we're running two community calls to cater different timezones for Q&A Office Hours:
-* Americas timezone: download the [calendar.ics](https://aka.ms/sk-community-calendar) file.
-* Asia Pacific timezone: download the [calendar-APAC.ics](https://aka.ms/sk-community-calendar-apac) file.
-
-Add Semantic Kernel Development Office Hours for Python and Java to your calendar to help with development:
-* Java Development Office Hours: [Java Development Office Hours](https://aka.ms/sk-java-dev-sync)
-* Python Development Office Hours: [Python Development Office Hours](https://aka.ms/sk-python-dev-sync)
-
-If you have any questions or if you would like to showcase your project(s), please email what you'd like us to cover here: skofficehours[at]microsoft.com.
-
-If you are unable to make it live, all meetings will be recorded and posted online.
-
## Join the conversation on Discord
We have a growing and active channel on Discord where you can get help, engage in lively discussion,
diff --git a/README.md b/README.md
index b748c348f..27585762d 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
[](https://github.com/microsoft/semantic-kernel-java/actions/workflows/java-build.yml)
-[](https://maven-badges.herokuapp.com/maven-central/com.microsoft.semantic-kernel/semantickernel-api)
[](https://github.com/microsoft/semantic-kernel-java/blob/main/LICENSE)
[](https://aka.ms/SKDiscord)
@@ -51,7 +50,7 @@ in a different direction, but also to consider the impact on the larger ecosyste
To learn more and get started:
- Read the [documentation](https://learn.microsoft.com/en-us/semantic-kernel/overview/?tabs=Java&pivots=programming-language-java)
-- Learn how to [contribute](https://learn.microsoft.com/en-us/semantic-kernel/get-started/contributing?tabs=Java&pivots=programming-language-java) to the project
+- Learn how to [contribute](https://learn.microsoft.com/en-us/semantic-kernel/support/contributing?tabs=Java&pivots=programming-language-java) to the project
- Join the [Discord community](https://aka.ms/SKDiscord)
- Attend [regular office hours and SK community events](COMMUNITY.md)
- Follow the team on our [blog](https://aka.ms/sk/blog)
diff --git a/connectors/semantickernel-connectors-web-bing/pom.xml b/agents/semantickernel-agents-core/pom.xml
similarity index 53%
rename from connectors/semantickernel-connectors-web-bing/pom.xml
rename to agents/semantickernel-agents-core/pom.xml
index 93eaf28a0..d5376209b 100644
--- a/connectors/semantickernel-connectors-web-bing/pom.xml
+++ b/agents/semantickernel-agents-core/pom.xml
@@ -1,19 +1,17 @@
-
4.0.0
-
com.microsoft.semantic-kernel
semantickernel-parent
- 1.0.0-rc2-SNAPSHOT
+ 1.5.1-SNAPSHOT
../../pom.xml
- semantickernel-connectors-web-bing
+ semantickernel-agents-core
- Semantic Kernel Bing Connector
- Defines Bing connectors to be used with the Semantic Kernel
+ Semantic Kernel Chat Completion Agent
+ Chat Completion Agent for Semantic Kernel
@@ -21,15 +19,17 @@
semantickernel-api
- com.fasterxml.jackson.core
- jackson-databind
- compile
+ com.microsoft.semantic-kernel
+ semantickernel-api-builders
- com.fasterxml.jackson.core
- jackson-core
- compile
+ com.microsoft.semantic-kernel
+ semantickernel-api-ai-services
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-exceptions
-
+
\ No newline at end of file
diff --git a/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java
new file mode 100644
index 000000000..b5294fe6b
--- /dev/null
+++ b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java
@@ -0,0 +1,329 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.agents.chatcompletion;
+
+import com.microsoft.semantickernel.Kernel;
+import com.microsoft.semantickernel.agents.AgentInvokeOptions;
+import com.microsoft.semantickernel.agents.AgentResponseItem;
+import com.microsoft.semantickernel.agents.AgentThread;
+import com.microsoft.semantickernel.agents.KernelAgent;
+import com.microsoft.semantickernel.builders.SemanticKernelBuilder;
+import com.microsoft.semantickernel.functionchoice.AutoFunctionChoiceBehavior;
+import com.microsoft.semantickernel.orchestration.InvocationContext;
+import com.microsoft.semantickernel.orchestration.InvocationReturnMode;
+import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
+import com.microsoft.semantickernel.semanticfunctions.PromptTemplate;
+import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
+import com.microsoft.semantickernel.semanticfunctions.PromptTemplateFactory;
+import com.microsoft.semantickernel.services.ServiceNotFoundException;
+import com.microsoft.semantickernel.services.chatcompletion.AuthorRole;
+import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
+import com.microsoft.semantickernel.services.chatcompletion.ChatHistory;
+import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ChatCompletionAgent extends KernelAgent {
+
+ private ChatCompletionAgent(
+ String id,
+ String name,
+ String description,
+ Kernel kernel,
+ KernelArguments kernelArguments,
+ InvocationContext context,
+ String instructions,
+ PromptTemplate template) {
+ super(
+ id,
+ name,
+ description,
+ kernel,
+ kernelArguments,
+ context,
+ instructions,
+ template);
+ }
+
+ /**
+ * Invoke the agent with the given chat history.
+ *
+ * @param messages The chat history to process
+ * @param thread The agent thread to use
+ * @param options The options for invoking the agent
+ * @return A Mono containing the agent response
+ */
+ @Override
+ public Mono>>> invokeAsync(
+ List> messages,
+ @Nullable AgentThread thread,
+ @Nullable AgentInvokeOptions options) {
+ return ensureThreadExistsWithMessagesAsync(messages, thread, ChatHistoryAgentThread::new)
+ .cast(ChatHistoryAgentThread.class)
+ .flatMap(agentThread -> {
+ // Extract the chat history from the thread
+ ChatHistory history = new ChatHistory(
+ agentThread.getChatHistory().getMessages());
+
+ // Invoke the agent with the chat history
+ return internalInvokeAsync(
+ history,
+ agentThread,
+ options)
+ .map(chatMessageContents -> chatMessageContents.stream()
+ .map(message -> new AgentResponseItem>(message,
+ agentThread))
+ .collect(Collectors.toList()));
+ });
+ }
+
+ private Mono>> internalInvokeAsync(
+ ChatHistory history,
+ AgentThread thread,
+ @Nullable AgentInvokeOptions options) {
+ if (options == null) {
+ options = new AgentInvokeOptions();
+ }
+
+ final Kernel kernel = options.getKernel() != null ? options.getKernel() : this.kernel;
+ final KernelArguments arguments = mergeArguments(options.getKernelArguments());
+ final String additionalInstructions = options.getAdditionalInstructions();
+ final InvocationContext invocationContext = options.getInvocationContext() != null
+ ? options.getInvocationContext()
+ : this.invocationContext;
+
+ try {
+ ChatCompletionService chatCompletionService = kernel
+ .getService(ChatCompletionService.class, arguments);
+
+ PromptExecutionSettings executionSettings = invocationContext != null
+ && invocationContext.getPromptExecutionSettings() != null
+ ? invocationContext.getPromptExecutionSettings()
+ : arguments.getExecutionSettings()
+ .get(chatCompletionService.getServiceId());
+
+ // Build base invocation context
+ InvocationContext.Builder builder = InvocationContext.builder()
+ .withPromptExecutionSettings(executionSettings)
+ .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY);
+
+ if (invocationContext != null) {
+ builder = builder
+ .withTelemetry(invocationContext.getTelemetry())
+ .withFunctionChoiceBehavior(invocationContext.getFunctionChoiceBehavior())
+ .withToolCallBehavior(invocationContext.getToolCallBehavior())
+ .withContextVariableConverter(invocationContext.getContextVariableTypes())
+ .withKernelHooks(invocationContext.getKernelHooks());
+ }
+
+ InvocationContext agentInvocationContext = builder.build();
+
+ return renderInstructionsAsync(kernel, arguments, agentInvocationContext).flatMap(
+ instructions -> {
+ // Create a new chat history with the instructions
+ ChatHistory chat = new ChatHistory(
+ instructions);
+
+ // Add agent additional instructions
+ if (additionalInstructions != null) {
+ chat.addMessage(new ChatMessageContent<>(
+ AuthorRole.SYSTEM,
+ additionalInstructions));
+ }
+
+ // Add the chat history to the new chat
+ chat.addAll(history);
+
+ // Retrieve the chat message contents asynchronously and notify the thread
+ if (shouldNotifyFunctionCalls(agentInvocationContext)) {
+ // Notify all messages including function calls
+ return chatCompletionService
+ .getChatMessageContentsAsync(chat, kernel, agentInvocationContext)
+ .flatMapMany(Flux::fromIterable)
+ .concatMap(message -> notifyThreadOfNewMessageAsync(thread, message)
+ .thenReturn(message))
+ // Filter out function calls and their results
+ .filter(message -> message.getContent() != null
+ && message.getAuthorRole() != AuthorRole.TOOL)
+ .collect(Collectors.toList());
+ }
+
+ // Return chat completion messages without notifying the thread
+ // We shouldn't add the function call content to the thread, since
+ // we don't know if the user will execute the call. They should add it themselves.
+ return chatCompletionService.getChatMessageContentsAsync(chat, kernel,
+ agentInvocationContext);
+ });
+
+ } catch (ServiceNotFoundException e) {
+ return Mono.error(e);
+ }
+ }
+
+ boolean shouldNotifyFunctionCalls(InvocationContext invocationContext) {
+ if (invocationContext == null) {
+ return false;
+ }
+
+ if (invocationContext.getFunctionChoiceBehavior() != null && invocationContext
+ .getFunctionChoiceBehavior() instanceof AutoFunctionChoiceBehavior) {
+ return ((AutoFunctionChoiceBehavior) invocationContext.getFunctionChoiceBehavior())
+ .isAutoInvoke();
+ }
+
+ if (invocationContext.getToolCallBehavior() != null) {
+ return invocationContext.getToolCallBehavior().isAutoInvokeAllowed();
+ }
+
+ return false;
+ }
+
+ @Override
+ public Mono notifyThreadOfNewMessageAsync(AgentThread thread,
+ ChatMessageContent> message) {
+ return Mono.defer(() -> {
+ return thread.onNewMessageAsync(message);
+ });
+ }
+
+ /**
+ * Builder for creating instances of ChatCompletionAgent.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder implements SemanticKernelBuilder {
+ private String id;
+ private String name;
+ private String description;
+ private Kernel kernel;
+ private KernelArguments kernelArguments;
+ private InvocationContext invocationContext;
+ private String instructions;
+ private PromptTemplate template;
+
+ /**
+ * Set the ID of the agent.
+ *
+ * @param id The ID of the agent.
+ */
+ public Builder withId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ /**
+ * Set the name of the agent.
+ *
+ * @param name The name of the agent.
+ */
+ public Builder withName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Set the description of the agent.
+ *
+ * @param description The description of the agent.
+ */
+ public Builder withDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * Set the kernel to use for the agent.
+ *
+ * @param kernel The kernel to use.
+ */
+ public Builder withKernel(Kernel kernel) {
+ this.kernel = kernel;
+ return this;
+ }
+
+ /**
+ * Set the kernel arguments to use for the agent.
+ *
+ * @param KernelArguments The kernel arguments to use.
+ */
+ @SuppressFBWarnings("EI_EXPOSE_REP2")
+ public Builder withKernelArguments(KernelArguments KernelArguments) {
+ this.kernelArguments = KernelArguments;
+ return this;
+ }
+
+ /**
+ * Set the instructions for the agent.
+ *
+ * @param instructions The instructions for the agent.
+ */
+ public Builder withInstructions(String instructions) {
+ this.instructions = instructions;
+ return this;
+ }
+
+ /**
+ * Set the invocation context for the agent.
+ *
+ * @param invocationContext The invocation context to use.
+ */
+ public Builder withInvocationContext(InvocationContext invocationContext) {
+ this.invocationContext = invocationContext;
+ return this;
+ }
+
+ /**
+ * Set the template for the agent.
+ *
+ * @param template The template to use.
+ */
+ public Builder withTemplate(PromptTemplate template) {
+ this.template = template;
+ return this;
+ }
+
+ /**
+ * Build the ChatCompletionAgent instance.
+ *
+ * @return The ChatCompletionAgent instance.
+ */
+ public ChatCompletionAgent build() {
+ return new ChatCompletionAgent(
+ id,
+ name,
+ description,
+ kernel,
+ kernelArguments,
+ invocationContext,
+ instructions,
+ template);
+ }
+
+ /**
+ * Build the ChatCompletionAgent instance with the given prompt template config and factory.
+ *
+ * @param promptTemplateConfig The prompt template config to use.
+ * @param promptTemplateFactory The prompt template factory to use.
+ * @return The ChatCompletionAgent instance.
+ */
+ public ChatCompletionAgent build(PromptTemplateConfig promptTemplateConfig,
+ PromptTemplateFactory promptTemplateFactory) {
+ return new ChatCompletionAgent(
+ id,
+ name,
+ description,
+ kernel,
+ kernelArguments,
+ invocationContext,
+ promptTemplateConfig.getTemplate(),
+ promptTemplateFactory.tryCreate(promptTemplateConfig));
+ }
+ }
+}
diff --git a/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java
new file mode 100644
index 000000000..6b3f62a9b
--- /dev/null
+++ b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.agents.chatcompletion;
+
+import com.microsoft.semantickernel.agents.AgentThread;
+import com.microsoft.semantickernel.agents.BaseAgentThread;
+import com.microsoft.semantickernel.builders.SemanticKernelBuilder;
+import com.microsoft.semantickernel.services.chatcompletion.ChatHistory;
+import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.UUID;
+
+public class ChatHistoryAgentThread extends BaseAgentThread {
+ private ChatHistory chatHistory;
+
+ /**
+ * Constructor for ChatHistoryAgentThread.
+ *
+ */
+ public ChatHistoryAgentThread() {
+ this(UUID.randomUUID().toString(), new ChatHistory());
+ }
+
+ /**
+ * Constructor for ChatHistoryAgentThread.
+ *
+ * @param chatHistory The chat history.
+ */
+ public ChatHistoryAgentThread(@Nullable ChatHistory chatHistory) {
+ this(UUID.randomUUID().toString(), chatHistory);
+ }
+
+ /**
+ * Constructor for ChatHistoryAgentThread.
+ *
+ * @param id The ID of the thread.
+ * @param chatHistory The chat history.
+ */
+ public ChatHistoryAgentThread(String id, @Nullable ChatHistory chatHistory) {
+ super(id);
+ this.chatHistory = chatHistory != null ? chatHistory : new ChatHistory();
+ }
+
+ /**
+ * Get the chat history.
+ *
+ * @return The chat history.
+ */
+ @SuppressFBWarnings("EI_EXPOSE_REP")
+ public ChatHistory getChatHistory() {
+ return chatHistory;
+ }
+
+ @Override
+ public Mono createAsync() {
+ if (this.id == null) {
+ this.id = UUID.randomUUID().toString();
+ chatHistory = new ChatHistory();
+ }
+ return Mono.just(id);
+ }
+
+ @Override
+ public Mono deleteAsync() {
+ return Mono.fromRunnable(chatHistory::clear);
+ }
+
+ /**
+ * Create a copy of the thread.
+ *
+ * @return A new instance of the thread.
+ */
+ @Override
+ public ChatHistoryAgentThread copy() {
+ return new ChatHistoryAgentThread(this.id, new ChatHistory(chatHistory.getMessages()));
+ }
+
+ @Override
+ public Mono onNewMessageAsync(ChatMessageContent> newMessage) {
+ return Mono.fromRunnable(() -> {
+ chatHistory.addMessage(newMessage);
+ });
+ }
+
+ public List> getMessages() {
+ return chatHistory.getMessages();
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder implements SemanticKernelBuilder {
+ private String id;
+ private ChatHistory chatHistory;
+
+ /**
+ * Set the ID of the thread.
+ *
+ * @param id The ID of the thread.
+ * @return The builder instance.
+ */
+ public Builder withId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ /**
+ * Set the chat history.
+ *
+ * @param chatHistory The chat history.
+ * @return The builder instance.
+ */
+ @SuppressFBWarnings("EI_EXPOSE_REP2")
+ public Builder withChatHistory(ChatHistory chatHistory) {
+ this.chatHistory = chatHistory;
+ return this;
+ }
+
+ @Override
+ public ChatHistoryAgentThread build() {
+ return new ChatHistoryAgentThread(id, chatHistory);
+ }
+ }
+}
diff --git a/aiservices/google/pom.xml b/aiservices/google/pom.xml
index f9ff65a49..e136a3eb3 100644
--- a/aiservices/google/pom.xml
+++ b/aiservices/google/pom.xml
@@ -4,7 +4,7 @@
com.microsoft.semantic-kernel
semantickernel-parent
- 1.2.2
+ 1.5.1-SNAPSHOT
../../pom.xml
@@ -17,7 +17,7 @@
com.google.cloud
libraries-bom
- 26.37.0
+ 26.80.0
pom
import
@@ -34,6 +34,22 @@
com.microsoft.semantic-kernel
semantickernel-api
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-builders
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-ai-services
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-exceptions
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-localization
+
com.fasterxml.jackson.core
diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiService.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiService.java
index 7155074f5..a65e96afb 100644
--- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiService.java
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiService.java
@@ -6,10 +6,18 @@
import javax.annotation.Nullable;
+/**
+ * Makes a Gemini service available to the Semantic Kernel.
+ */
public class GeminiService implements AIService {
private final VertexAI client;
private final String modelId;
+ /**
+ * Creates a new Gemini service.
+ * @param client The VertexAI client
+ * @param modelId The Gemini model ID
+ */
protected GeminiService(VertexAI client, String modelId) {
this.client = client;
this.modelId = modelId;
@@ -27,6 +35,10 @@ public String getServiceId() {
return null;
}
+ /**
+ * Gets the VertexAI client.
+ * @return The VertexAI client
+ */
protected VertexAI getClient() {
return client;
}
diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiServiceBuilder.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiServiceBuilder.java
index c5d3f6584..3312299cc 100644
--- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiServiceBuilder.java
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/GeminiServiceBuilder.java
@@ -7,6 +7,8 @@
/**
* Builder for a Gemini service.
+ * @param The type of the service
+ * @param The type of the builder
*/
public abstract class GeminiServiceBuilder> implements
SemanticKernelBuilder {
diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiChatCompletion.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiChatCompletion.java
index 8aa669f07..2bd45c022 100644
--- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiChatCompletion.java
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiChatCompletion.java
@@ -16,11 +16,13 @@
import com.google.protobuf.Value;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.aiservices.google.GeminiService;
+import com.microsoft.semantickernel.aiservices.google.GeminiServiceBuilder;
import com.microsoft.semantickernel.aiservices.google.implementation.MonoConverter;
import com.microsoft.semantickernel.contextvariables.ContextVariableTypes;
import com.microsoft.semantickernel.exceptions.AIException;
import com.microsoft.semantickernel.exceptions.SKCheckedException;
import com.microsoft.semantickernel.exceptions.SKException;
+import com.microsoft.semantickernel.localization.SemanticKernelResources;
import com.microsoft.semantickernel.orchestration.FunctionResult;
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
import com.microsoft.semantickernel.orchestration.InvocationContext;
@@ -30,12 +32,12 @@
import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.semanticfunctions.InputVariable;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
-import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
import com.microsoft.semantickernel.services.chatcompletion.AuthorRole;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.services.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent;
-import com.microsoft.semantickernel.aiservices.google.GeminiServiceBuilder;
+import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
@@ -48,10 +50,18 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+/**
+ * A chat completion service that uses the Gemini model to generate chat completions.
+ */
public class GeminiChatCompletion extends GeminiService implements ChatCompletionService {
private static final Logger LOGGER = LoggerFactory.getLogger(GeminiChatCompletion.class);
+ /**
+ * Constructor for {@link GeminiChatCompletion}.
+ * @param client The VertexAI client
+ * @param modelId The model ID
+ */
public GeminiChatCompletion(VertexAI client, String modelId) {
super(client, modelId);
}
@@ -74,6 +84,53 @@ public Mono>> getChatMessageContentsAsync(String prom
invocationContext);
}
+ @Override
+ public Flux> getStreamingChatMessageContentsAsync(
+ ChatHistory chatHistory,
+ @Nullable Kernel kernel,
+ @Nullable InvocationContext invocationContext) {
+
+ LOGGER.warn("Streaming has been called on GeminiChatCompletion service. "
+ + "This is currently not supported in Gemini. "
+ + "The results will be returned in a non streaming fashion.");
+
+ return getChatMessageContentsAsync(chatHistory, kernel, invocationContext)
+ .flatMapIterable(chatMessageContents -> chatMessageContents)
+ .map(content -> {
+ return new GeminiStreamingChatMessageContent(
+ content.getAuthorRole(),
+ content.getContent(),
+ getModelId(),
+ content.getInnerContent(),
+ content.getEncoding(),
+ content.getMetadata(),
+ null,
+ UUID.randomUUID().toString());
+ });
+ }
+
+ @Override
+ public Flux> getStreamingChatMessageContentsAsync(String prompt,
+ @Nullable Kernel kernel, @Nullable InvocationContext invocationContext) {
+ LOGGER.warn("Streaming has been called on GeminiChatCompletion service. "
+ + "This is currently not supported in Gemini. "
+ + "The results will be returned in a non streaming fashion.");
+
+ return getChatMessageContentsAsync(prompt, kernel, invocationContext)
+ .flatMapIterable(chatMessageContents -> chatMessageContents)
+ .map(content -> {
+ return new GeminiStreamingChatMessageContent(
+ content.getAuthorRole(),
+ content.getContent(),
+ getModelId(),
+ content.getInnerContent(),
+ content.getEncoding(),
+ content.getMetadata(),
+ null,
+ UUID.randomUUID().toString());
+ });
+ }
+
@Override
public Mono>> getChatMessageContentsAsync(ChatHistory chatHistory,
@Nullable Kernel kernel, @Nullable InvocationContext invocationContext) {
@@ -97,7 +154,8 @@ private Mono>> internalChatMessageContentsAsync(
try {
GenerativeModel model = getGenerativeModel(kernel, invocationContext);
return MonoConverter.fromApiFuture(model.generateContentAsync(contents))
- .doOnError(e -> LOGGER.error("Error generating chat completion", e))
+ .doOnError(e -> LOGGER.error(
+ SemanticKernelResources.getString("error.generating.chat.completion"), e))
.flatMap(result -> {
// Get ChatMessageContent from the response
GeminiChatMessageContent> response = getGeminiChatMessageContentFromResponse(
@@ -253,7 +311,8 @@ private GenerativeModel getGenerativeModel(@Nullable Kernel kernel,
if (settings.getResultsPerPrompt() < 1
|| settings.getResultsPerPrompt() > MAX_RESULTS_PER_PROMPT) {
- throw SKCheckedException.build("Error building generative model.",
+ throw SKCheckedException.build(
+ SemanticKernelResources.getString("error.building.generative.model"),
new AIException(AIException.ErrorCodes.INVALID_REQUEST,
String.format(
"Results per prompt must be in range between 1 and %d, inclusive.",
@@ -340,6 +399,13 @@ private Tool getTool(@Nullable Kernel kernel, @Nullable ToolCallBehavior toolCal
return toolBuilder.build();
}
+ /**
+ * Invoke the Gemini function call.
+ * @param kernel The semantic kernel
+ * @param invocationContext Additional context for the invocation
+ * @param geminiFunction The Gemini function call
+ * @return The result of the function call
+ */
public Mono performFunctionCall(@Nullable Kernel kernel,
@Nullable InvocationContext invocationContext, GeminiFunctionCall geminiFunction) {
if (kernel == null) {
@@ -370,7 +436,7 @@ public Mono performFunctionCall(@Nullable Kernel kernel,
? new ContextVariableTypes()
: invocationContext.getContextVariableTypes();
- KernelFunctionArguments.Builder arguments = KernelFunctionArguments.builder();
+ KernelArguments.Builder arguments = KernelArguments.builder();
geminiFunction.getFunctionCall().getArgs().getFieldsMap().forEach((key, value) -> {
arguments.withVariable(key, value.getStringValue());
});
@@ -382,6 +448,9 @@ public Mono performFunctionCall(@Nullable Kernel kernel,
.map(result -> new GeminiFunctionCall(geminiFunction.getFunctionCall(), result));
}
+ /**
+ * Builder for {@link GeminiChatCompletion}.
+ */
public static class Builder extends GeminiServiceBuilder {
@Override
diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiFunctionCall.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiFunctionCall.java
index fc64c8307..0a6e87d4e 100644
--- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiFunctionCall.java
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiFunctionCall.java
@@ -9,6 +9,9 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+/**
+ * Represents a function call in Gemini.
+ */
public class GeminiFunctionCall {
@Nonnull
private final FunctionCall functionCall;
@@ -17,6 +20,11 @@ public class GeminiFunctionCall {
private final String pluginName;
private final String functionName;
+ /**
+ * Creates a new Gemini function call.
+ * @param functionCall The function call
+ * @param functionResult The result of the function invocation
+ */
@SuppressFBWarnings("EI_EXPOSE_REP2")
public GeminiFunctionCall(
@Nonnull FunctionCall functionCall,
@@ -29,19 +37,35 @@ public GeminiFunctionCall(
this.functionName = name[1];
}
+ /**
+ * Gets the plugin name.
+ * @return The plugin name
+ */
public String getPluginName() {
return pluginName;
}
+ /**
+ * Gets the function name.
+ * @return The function name
+ */
public String getFunctionName() {
return functionName;
}
+ /**
+ * Gets the function call.
+ * @return The function call
+ */
@SuppressFBWarnings("EI_EXPOSE_REP")
public FunctionCall getFunctionCall() {
return functionCall;
}
+ /**
+ * Gets the function result.
+ * @return The function result
+ */
@Nullable
public FunctionResult> getFunctionResult() {
return functionResult;
diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiRole.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiRole.java
index 745907239..03983553b 100644
--- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiRole.java
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiRole.java
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.aiservices.google.chatcompletion;
+/**
+ * Represents the role of a message in a Gemini conversation.
+ */
public enum GeminiRole {
/**
* A user message is a message generated by the user.
diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiStreamingChatMessageContent.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiStreamingChatMessageContent.java
new file mode 100644
index 000000000..e6f0d5511
--- /dev/null
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiStreamingChatMessageContent.java
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.google.chatcompletion;
+
+import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
+import com.microsoft.semantickernel.services.chatcompletion.AuthorRole;
+import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent;
+import java.nio.charset.Charset;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Represents the content of a chat message.
+ *
+ * @param The type of the inner content.
+ */
+public class GeminiStreamingChatMessageContent extends GeminiChatMessageContent implements
+ StreamingChatContent {
+
+ private final String id;
+
+ /**
+ * Creates a new instance of the {@link GeminiChatMessageContent} class.
+ *
+ * @param authorRole The author role that generated the content.
+ * @param content The content.
+ * @param modelId The model id.
+ * @param innerContent The inner content.
+ * @param encoding The encoding.
+ * @param metadata The metadata.
+ * @param id The id of the message.
+ * @param geminiFunctionCalls The function calls.
+ */
+ public GeminiStreamingChatMessageContent(AuthorRole authorRole, String content,
+ @Nullable String modelId, @Nullable T innerContent, @Nullable Charset encoding,
+ @Nullable FunctionResultMetadata metadata,
+ @Nullable List geminiFunctionCalls,
+ String id) {
+ super(authorRole, content, modelId, innerContent, encoding, metadata, geminiFunctionCalls);
+ this.id = id;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+}
diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiXMLPromptParser.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiXMLPromptParser.java
index afde9a331..e43032dc1 100644
--- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiXMLPromptParser.java
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/chatcompletion/GeminiXMLPromptParser.java
@@ -17,15 +17,26 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Parses an XML prompt for a Gemini chat.
+ */
public class GeminiXMLPromptParser {
private static final Logger LOGGER = LoggerFactory.getLogger(GeminiXMLPromptParser.class);
+ /**
+ * Represents a parsed prompt for Gemini chat.
+ */
public static class GeminiParsedPrompt {
private final ChatHistory chatHistory;
private final List functions;
+ /**
+ * Creates a new parsed prompt.
+ * @param parsedChatHistory The chat history
+ * @param parsedFunctions The functions declarations.
+ */
protected GeminiParsedPrompt(
ChatHistory parsedChatHistory,
@Nullable List parsedFunctions) {
@@ -36,10 +47,18 @@ protected GeminiParsedPrompt(
this.functions = parsedFunctions;
}
+ /**
+ * Gets the chat history.
+ * @return A copy of the chat history.
+ */
public ChatHistory getChatHistory() {
return new ChatHistory(chatHistory.getMessages());
}
+ /**
+ * Gets the functions declarations.
+ * @return A copy of the functions declarations.
+ */
public List getFunctions() {
return Collections.unmodifiableList(functions);
}
@@ -131,6 +150,11 @@ public ChatPromptParseVisitor reset() {
}
}
+ /**
+ * Create a GeminiParsedPrompt by parsing a raw prompt.
+ * @param rawPrompt the raw prompt to parse.
+ * @return The parsed prompt.
+ */
public static GeminiParsedPrompt parse(String rawPrompt) {
ChatPromptParseVisitor visitor = ChatXMLPromptParser.parse(
rawPrompt,
diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/StreamingTextContent.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiStreamingTextContent.java
similarity index 63%
rename from semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/StreamingTextContent.java
rename to aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiStreamingTextContent.java
index 7fdadad79..9bf9a6fd3 100644
--- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/textcompletion/StreamingTextContent.java
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiStreamingTextContent.java
@@ -1,13 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
-package com.microsoft.semantickernel.services.textcompletion;
+package com.microsoft.semantickernel.aiservices.google.textcompletion;
-import com.microsoft.semantickernel.services.StreamingKernelContent;
+import com.microsoft.semantickernel.services.StreamingTextContent;
+import com.microsoft.semantickernel.services.textcompletion.TextContent;
import javax.annotation.Nullable;
/**
* StreamingTextContent is a wrapper for TextContent that allows for streaming.
*/
-public class StreamingTextContent extends StreamingKernelContent {
+public class GeminiStreamingTextContent extends StreamingTextContent {
/**
* Initializes a new instance of the {@code StreamingTextContent} class with a provided text
@@ -15,7 +16,7 @@ public class StreamingTextContent extends StreamingKernelContent {
*
* @param content The text content.
*/
- public StreamingTextContent(TextContent content) {
+ public GeminiStreamingTextContent(TextContent content) {
super(content, 0, null, null);
}
diff --git a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiTextGenerationService.java b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiTextGenerationService.java
index 4d7c2a254..5e5551161 100644
--- a/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiTextGenerationService.java
+++ b/aiservices/google/src/main/java/com/microsoft/semantickernel/aiservices/google/textcompletion/GeminiTextGenerationService.java
@@ -7,36 +7,48 @@
import com.google.cloud.vertexai.generativeai.GenerativeModel;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.aiservices.google.GeminiService;
+import com.microsoft.semantickernel.aiservices.google.GeminiServiceBuilder;
import com.microsoft.semantickernel.aiservices.google.implementation.MonoConverter;
import com.microsoft.semantickernel.exceptions.AIException;
import com.microsoft.semantickernel.exceptions.SKCheckedException;
import com.microsoft.semantickernel.exceptions.SKException;
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
-import com.microsoft.semantickernel.aiservices.google.GeminiServiceBuilder;
-import com.microsoft.semantickernel.services.textcompletion.StreamingTextContent;
+import com.microsoft.semantickernel.services.StreamingTextContent;
import com.microsoft.semantickernel.services.textcompletion.TextContent;
import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.annotation.Nullable;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
+import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+/**
+ * A Gemini service for text generation.
+ * @see TextGenerationService
+ */
public class GeminiTextGenerationService extends GeminiService implements TextGenerationService {
+
private static final Logger LOGGER = LoggerFactory.getLogger(GeminiTextGenerationService.class);
+ /**
+ * Creates a new Gemini text generation service.
+ * @param client The VertexAI client
+ * @param modelId The Gemini model ID
+ */
public GeminiTextGenerationService(VertexAI client, String modelId) {
super(client, modelId);
}
+ /**
+ * Creates a new builder for a Gemini text generation service.
+ * @return The builder
+ */
public static Builder builder() {
return new Builder();
}
@@ -57,7 +69,7 @@ public Flux getStreamingTextContentsAsync(
return this
.internalGetTextAsync(prompt, executionSettings)
.flatMapMany(it -> Flux.fromStream(it.stream())
- .map(StreamingTextContent::new));
+ .map(GeminiStreamingTextContent::new));
}
private Mono> internalGetTextAsync(String prompt,
@@ -122,8 +134,12 @@ private GenerativeModel getGenerativeModel(
return modelBuilder.build();
}
+ /**
+ * Builder for a Gemini text generation service.
+ */
public static class Builder extends
GeminiServiceBuilder {
+
@Override
public GeminiTextGenerationService build() {
if (this.client == null) {
diff --git a/aiservices/huggingface/pom.xml b/aiservices/huggingface/pom.xml
index ccfbb6891..82f5cd52d 100644
--- a/aiservices/huggingface/pom.xml
+++ b/aiservices/huggingface/pom.xml
@@ -6,7 +6,7 @@
com.microsoft.semantic-kernel
semantickernel-parent
- 1.2.2
+ 1.5.1-SNAPSHOT
../../pom.xml
@@ -31,6 +31,14 @@
com.microsoft.semantic-kernel
semantickernel-api
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-exceptions
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-ai-services
+
com.azure
azure-core
diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/HuggingFaceClient.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/HuggingFaceClient.java
index 93fe64fdf..4e8f9bb4d 100644
--- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/HuggingFaceClient.java
+++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/HuggingFaceClient.java
@@ -19,12 +19,21 @@
import reactor.core.publisher.Mono;
import javax.annotation.Nullable;
+/**
+ * A client for the Hugging Face API.
+ */
public class HuggingFaceClient {
private final KeyCredential key;
private final String endpoint;
private final HttpClient httpClient;
+ /**
+ * Creates a new Hugging Face client.
+ * @param key The key credential for endpoint authentication.
+ * @param endpoint The endpoint for the Hugging Face API.
+ * @param httpClient The HTTP client to use for requests.
+ */
public HuggingFaceClient(
KeyCredential key,
String endpoint,
@@ -74,6 +83,12 @@ public GeneratedTextItemList(
}
+ /**
+ * Gets the text contents from the Hugging Face API.
+ * @param modelId The model ID.
+ * @param textGenerationRequest The text generation request.
+ * @return The generated text items.
+ */
public Mono> getTextContentsAsync(
String modelId,
TextGenerationRequest textGenerationRequest) {
@@ -131,10 +146,17 @@ private Mono performRequest(String modelId,
return responseBody;
}
+ /**
+ * Creates a new builder for a Hugging Face client.
+ * @return The builder
+ */
public static Builder builder() {
return new Builder();
}
+ /**
+ * Builder for a Hugging Face client.
+ */
public static class Builder {
@Nullable
@@ -144,6 +166,10 @@ public static class Builder {
@Nullable
private HttpClient httpClient = null;
+ /**
+ * Builds the Hugging Face client.
+ * @return The client
+ */
public HuggingFaceClient build() {
if (httpClient == null) {
httpClient = HttpClient.createDefault();
@@ -160,16 +186,31 @@ public HuggingFaceClient build() {
httpClient);
}
+ /**
+ * Sets the key credential for the client.
+ * @param key The key credential
+ * @return The builder
+ */
public Builder credential(KeyCredential key) {
this.key = key;
return this;
}
+ /**
+ * Sets the endpoint for the client.
+ * @param endpoint The endpoint
+ * @return The builder
+ */
public Builder endpoint(String endpoint) {
this.endpoint = endpoint;
return this;
}
+ /**
+ * Sets the HTTP client for the client.
+ * @param httpClient The HTTP client
+ * @return The builder
+ */
public Builder httpClient(HttpClient httpClient) {
this.httpClient = httpClient;
return this;
diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/GeneratedTextItem.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/GeneratedTextItem.java
index 6f4125c63..12ed5be60 100644
--- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/GeneratedTextItem.java
+++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/GeneratedTextItem.java
@@ -8,6 +8,9 @@
import java.util.List;
import javax.annotation.Nullable;
+/**
+ * Represents a generated text item deserialized from a JSON response.
+ */
public class GeneratedTextItem {
@Nullable
@@ -18,6 +21,11 @@ public class GeneratedTextItem {
@JsonProperty("details")
private final TextGenerationDetails details;
+ /**
+ * Constructor used by Jackson to deserialize a generated text item.
+ * @param generatedText The generated text.
+ * @param details The details of the generation.
+ */
@JsonCreator
public GeneratedTextItem(
@JsonProperty("generated_text") @Nullable String generatedText,
@@ -26,16 +34,27 @@ public GeneratedTextItem(
this.details = details;
}
+ /**
+ * Gets the generated text.
+ * @return The generated text.
+ */
@Nullable
public String getGeneratedText() {
return generatedText;
}
+ /**
+ * Gets the details of the generation.
+ * @return The details of the generation.
+ */
@Nullable
public TextGenerationDetails getDetails() {
return details;
}
+ /**
+ * Represents the details of a text generation deserialized from a JSON response.
+ */
public static class TextGenerationDetails {
@Nullable
@@ -57,6 +76,14 @@ public static class TextGenerationDetails {
@JsonProperty("tokens")
private final List tokens;
+ /**
+ * Constructor used by Jackson to deserialize text generation details.
+ * @param finishReason The reason the generation finished.
+ * @param generatedTokens The number of tokens generated.
+ * @param seed The seed used for generation.
+ * @param prefill The prefill tokens.
+ * @param tokens The generated tokens.
+ */
@JsonCreator
public TextGenerationDetails(
@JsonProperty("finish_reason") @Nullable String finishReason,
@@ -79,31 +106,54 @@ public TextGenerationDetails(
}
}
+ /**
+ * Gets the reason the generation finished.
+ * @return The reason the generation finished.
+ */
@Nullable
public String getFinishReason() {
return finishReason;
}
+ /**
+ * Gets the number of tokens generated.
+ * @return The number of tokens generated.
+ */
public int getGeneratedTokens() {
return generatedTokens;
}
+ /**
+ * Gets the seed used for generation.
+ * @return The seed used for generation.
+ */
@Nullable
public Long getSeed() {
return seed;
}
+ /**
+ * Gets the prefill tokens.
+ * @return The prefill tokens.
+ */
@Nullable
public List getPrefill() {
return Collections.unmodifiableList(prefill);
}
+ /**
+ * Gets the generated tokens.
+ * @return The generated tokens.
+ */
@Nullable
public List getTokens() {
return Collections.unmodifiableList(tokens);
}
}
+ /**
+ * Represents a prefill token deserialized from a JSON response.
+ */
public static class TextGenerationPrefillToken {
@JsonProperty("id")
@@ -116,6 +166,12 @@ public static class TextGenerationPrefillToken {
@JsonProperty("logprob")
private final double logProb;
+ /**
+ * Constructor used by Jackson to deserialize a prefill token.
+ * @param id The token ID.
+ * @param text The token text.
+ * @param logProb The log probability of the token.
+ */
@JsonCreator
public TextGenerationPrefillToken(
@JsonProperty("id") int id,
@@ -126,25 +182,47 @@ public TextGenerationPrefillToken(
this.logProb = logProb;
}
+ /**
+ * Gets the token ID.
+ * @return The token ID.
+ */
public int getId() {
return id;
}
+ /**
+ * Gets the token text.
+ * @return The token text.
+ */
@Nullable
public String getText() {
return text;
}
+ /**
+ * Gets the log probability of the token.
+ * @return The log probability of the token.
+ */
public double getLogProb() {
return logProb;
}
}
+ /**
+ * Represents a generated token deserialized from a JSON response.
+ */
public static class TextGenerationToken extends TextGenerationPrefillToken {
@JsonProperty("special")
private final boolean special;
+ /**
+ * Constructor used by Jackson to deserialize a generated token.
+ * @param special Whether the token is special.
+ * @param id The token ID.
+ * @param text The token text.
+ * @param logProb The log probability of the token.
+ */
@JsonCreator
public TextGenerationToken(
@JsonProperty("special") boolean special,
@@ -155,6 +233,10 @@ public TextGenerationToken(
this.special = special;
}
+ /**
+ * Gets whether the token is special.
+ * @return Whether the token is special.
+ */
public boolean isSpecial() {
return special;
}
diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/TextGenerationRequest.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/TextGenerationRequest.java
index fd0b60778..0e52b5681 100644
--- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/TextGenerationRequest.java
+++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/models/TextGenerationRequest.java
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.aiservices.huggingface.models;
-import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -11,6 +10,9 @@
import java.util.Arrays;
import java.util.List;
+/**
+ * Represents a request to generate text using the Hugging Face API.
+ */
@JsonInclude(Include.NON_NULL)
public class TextGenerationRequest {
@@ -42,6 +44,13 @@ public class TextGenerationRequest {
@JsonProperty("options")
private final HuggingFaceTextOptions options;
+ /**
+ * Create a new instance of TextGenerationRequest.
+ * @param inputs The input string to generate text for.
+ * @param stream Enable streaming.
+ * @param parameters Parameters used by the model for generation.
+ * @param options Options used by the model for generation.
+ */
public TextGenerationRequest(
@Nullable String inputs,
boolean stream,
@@ -53,6 +62,12 @@ public TextGenerationRequest(
this.options = options;
}
+ /**
+ * Create a new instance of TextGenerationRequest.
+ * @param prompt The prompt to generate text for.
+ * @param executionSettings The settings for executing the prompt.
+ * @return A new instance of TextGenerationRequest.
+ */
public static TextGenerationRequest fromPromptAndExecutionSettings(String prompt,
HuggingFacePromptExecutionSettings executionSettings) {
return new TextGenerationRequest(
@@ -72,6 +87,9 @@ public static TextGenerationRequest fromPromptAndExecutionSettings(String prompt
new HuggingFaceTextOptions());
}
+ /**
+ * Parameters used by the model for generation.
+ */
public static class HuggingFaceTextParameters {
///
@@ -156,6 +174,19 @@ public static class HuggingFaceTextParameters {
@JsonProperty("details")
private final Boolean details;
+ /**
+ * Creator method for jackson deserialization.
+ * @param topK The number of top tokens considered within the sample operation to create new text.
+ * @param topP The tokens that are within the sample operation of text generation.
+ * @param temperature The temperature of the sampling operation.
+ * @param repetitionPenalty The repetition penalty.
+ * @param maxNewTokens The amount of new tokens to be generated.
+ * @param maxTime The amount of time in seconds that the query should take maximum.
+ * @param returnFullText A value indicating whether the return results will contain the original query.
+ * @param numReturnSequences The number of propositions to be returned.
+ * @param doSample A value indicating whether to use sampling.
+ * @param details A value indicating whether to include the details of the generation.
+ */
public HuggingFaceTextParameters(
@JsonProperty("top_k") @Nullable Integer topK,
@JsonProperty("top_p") @Nullable Double topP,
@@ -179,56 +210,99 @@ public HuggingFaceTextParameters(
this.details = details;
}
+ /**
+ * Gets the number of top tokens considered within the sample operation to create new text.
+ * @return The number of top tokens considered within the sample operation to create new text.
+ */
@Nullable
public Integer getTopK() {
return topK;
}
+ /**
+ * Gets the tokens that are within the sample operation of text generation.
+ * @return The tokens that are within the sample operation of text generation.
+ */
@Nullable
public Double getTopP() {
return topP;
}
+ /**
+ * Gets the temperature of the sampling operation.
+ * @return The temperature of the sampling operation.
+ */
@Nullable
public Double getTemperature() {
return temperature;
}
+ /**
+ * Gets the repetition penalty.
+ * @return The repetition penalty.
+ */
@Nullable
public Double getRepetitionPenalty() {
return repetitionPenalty;
}
+ /**
+ * Gets the amount of new tokens to be generated.
+ * @return The amount of new tokens to be generated.
+ */
@Nullable
public Integer getMaxNewTokens() {
return maxNewTokens;
}
+ /**
+ * Gets the amount of time in seconds that the query should take maximum.
+ * @return The amount of time in seconds that the query should take maximum.
+ */
@Nullable
public Double getMaxTime() {
return maxTime;
}
+ /**
+ * Gets a value indicating whether the return results will contain the original query.
+ * @return A value indicating whether the return results will contain the original query.
+ */
public boolean isReturnFullText() {
return returnFullText;
}
+ /**
+ * Gets the number of propositions to be returned.
+ * @return The number of propositions to be returned.
+ */
@Nullable
public Integer getNumReturnSequences() {
return numReturnSequences;
}
+ /**
+ * Gets a value indicating whether to use sampling.
+ * @return A value indicating whether to use sampling.
+ */
@Nullable
public Boolean getDoSample() {
return doSample;
}
+ /**
+ * Gets a value indicating whether to include the details of the generation.
+ * @return A value indicating whether to include the details of the generation.
+ */
@Nullable
public Boolean getDetails() {
return details;
}
}
+ /**
+ * Options used by the model for generation.
+ */
@SuppressFBWarnings("SS_SHOULD_BE_STATIC")
public static class HuggingFaceTextOptions {
@@ -249,10 +323,18 @@ public static class HuggingFaceTextOptions {
@JsonProperty("wait_for_model")
private final boolean waitForModel = false;
+ /**
+ * Gets a value indicating whether to use the cache layer on the inference API.
+ * @return A value indicating whether to use the cache layer on the inference API.
+ */
public boolean isUseCache() {
return useCache;
}
+ /**
+ * Gets a value indicating whether to wait for the model if it is not ready.
+ * @return A value indicating whether to wait for the model if it is not ready.
+ */
public boolean isWaitForModel() {
return waitForModel;
}
diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFacePromptExecutionSettings.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFacePromptExecutionSettings.java
index ed3a4df5e..ac4f4c093 100644
--- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFacePromptExecutionSettings.java
+++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFacePromptExecutionSettings.java
@@ -2,11 +2,15 @@
package com.microsoft.semantickernel.aiservices.huggingface.services;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
+import com.microsoft.semantickernel.orchestration.responseformat.ResponseFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
+/**
+ * Represents the settings for executing a prompt with the Hugging Face API.
+ */
public class HuggingFacePromptExecutionSettings extends PromptExecutionSettings {
@Nullable
@@ -24,6 +28,11 @@ public class HuggingFacePromptExecutionSettings extends PromptExecutionSettings
@Nullable
private final Long seed;
+ /**
+ * Create a new instance of HuggingFacePromptExecutionSettings.
+ *
+ * @param copy The PromptExecutionSettings to copy.
+ */
public HuggingFacePromptExecutionSettings(PromptExecutionSettings copy) {
super(
copy.getServiceId(),
@@ -38,7 +47,8 @@ public HuggingFacePromptExecutionSettings(PromptExecutionSettings copy) {
copy.getUser(),
copy.getStopSequences(),
copy.getTokenSelectionBiases(),
- copy.getResponseFormat() == null ? null : copy.getResponseFormat().toString());
+ copy.getResponseFormat() == null ? null : copy.getResponseFormat(),
+ copy.getMaxCompletionTokens() == null ? null : copy.getMaxCompletionTokens().toString());
this.topK = null;
this.repetitionPenalty = null;
this.maxTime = null;
@@ -64,6 +74,13 @@ public HuggingFacePromptExecutionSettings(PromptExecutionSettings copy) {
* @param stopSequences The stop sequences to use for prompt execution.
* @param tokenSelectionBiases The token selection biases to use for prompt execution.
* @param responseFormat The response format to use for prompt execution
+ * @param topK The topK setting for prompt execution.
+ * @param repetitionPenalty The repetition penalty setting for prompt execution.
+ * @param maxTime The max time setting for prompt execution.
+ * @param details The details setting for prompt execution.
+ * @param logProbs The logprobs setting for prompt execution.
+ * @param topLogProbs The top log probs setting for prompt execution.
+ * @param seed The seed setting for prompt execution
*/
public HuggingFacePromptExecutionSettings(
String serviceId,
@@ -78,17 +95,19 @@ public HuggingFacePromptExecutionSettings(
String user,
@Nullable List stopSequences,
@Nullable Map tokenSelectionBiases,
- @Nullable String responseFormat,
+ @Nullable ResponseFormat responseFormat,
@Nullable Integer topK,
@Nullable Double repetitionPenalty,
@Nullable Double maxTime,
@Nullable Boolean details,
@Nullable Boolean logProbs,
@Nullable Integer topLogProbs,
- @Nullable Long seed) {
+ @Nullable Long seed,
+ @Nullable Boolean maxCompletionTokensEnable) {
super(
serviceId, modelId, temperature, topP, presencePenalty, frequencyPenalty, maxTokens,
- resultsPerPrompt, bestOf, user, stopSequences, tokenSelectionBiases, responseFormat);
+ resultsPerPrompt, bestOf, user, stopSequences, tokenSelectionBiases, responseFormat,
+ Boolean.toString(Boolean.TRUE.equals(maxCompletionTokensEnable)));
this.topK = topK;
this.repetitionPenalty = repetitionPenalty;
@@ -99,6 +118,14 @@ public HuggingFacePromptExecutionSettings(
this.seed = seed;
}
+ /**
+ * Create a new instance of PromptExecutionSettings from a PromptExecutionSettings.
+ * This method handles the whether the PromptExecutionSettings is already a
+ * HuggingFacePromptExecutionSettings or a new instance needs to be created
+ * from the provided PromptExecutionSettings.
+ * @param promptExecutionSettings The PromptExecutionSettings to copy.
+ * @return The PromptExecutionSettings mapped to a HuggingFacePromptExecutionSettings.
+ */
public static HuggingFacePromptExecutionSettings fromExecutionSettings(
PromptExecutionSettings promptExecutionSettings) {
if (promptExecutionSettings instanceof HuggingFacePromptExecutionSettings) {
@@ -119,7 +146,7 @@ public static HuggingFacePromptExecutionSettings fromExecutionSettings(
promptExecutionSettings.getStopSequences(),
promptExecutionSettings.getTokenSelectionBiases(),
promptExecutionSettings.getResponseFormat() != null
- ? promptExecutionSettings.getResponseFormat().toString()
+ ? promptExecutionSettings.getResponseFormat()
: null,
null,
null,
@@ -127,39 +154,68 @@ public static HuggingFacePromptExecutionSettings fromExecutionSettings(
null,
null,
null,
+ null,
null);
}
+ /**
+ * Gets the topK setting for prompt execution.
+ * @return The topK setting for prompt execution
+ */
@Nullable
public Integer getTopK() {
return topK;
}
+ /**
+ * Gets the repetition penalty setting for prompt execution.
+ * @return The repetition penalty setting for prompt execution
+ */
@Nullable
public Double getRepetitionPenalty() {
return repetitionPenalty;
}
+ /**
+ * Gets the max time setting for prompt execution.
+ * @return The max time setting for prompt execution
+ */
@Nullable
public Double getMaxTime() {
return maxTime;
}
+ /**
+ * Gets the details setting for prompt execution.
+ * @return The details setting for prompt execution
+ */
@Nullable
public Boolean getDetails() {
return details;
}
+ /**
+ * Gets the logprobs setting for prompt execution.
+ * @return The logprobs setting for prompt execution
+ */
@Nullable
public Boolean getLogprobs() {
return logProbs;
}
+ /**
+ * Gets the top log probs setting for prompt execution.
+ * @return The top log probs setting for prompt execution
+ */
@Nullable
public Integer getTopLogProbs() {
return topLogProbs;
}
+ /**
+ * Gets the seed setting for prompt execution.
+ * @return The seed setting for prompt execution
+ */
@Nullable
public Long getSeed() {
return seed;
diff --git a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFaceTextGenerationService.java b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFaceTextGenerationService.java
index 96e05ff99..a08b42326 100644
--- a/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFaceTextGenerationService.java
+++ b/aiservices/huggingface/src/main/java/com/microsoft/semantickernel/aiservices/huggingface/services/HuggingFaceTextGenerationService.java
@@ -9,7 +9,7 @@
import com.microsoft.semantickernel.exceptions.SKException;
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
-import com.microsoft.semantickernel.services.textcompletion.StreamingTextContent;
+import com.microsoft.semantickernel.services.StreamingTextContent;
import com.microsoft.semantickernel.services.textcompletion.TextContent;
import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
import java.util.List;
@@ -19,12 +19,21 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+/**
+ * A service that generates text using the Hugging Face API.
+ */
public class HuggingFaceTextGenerationService implements TextGenerationService {
private final String modelId;
private final String serviceId;
private final HuggingFaceClient client;
+ /**
+ * Create a new instance of HuggingFaceTextGenerationService.
+ * @param modelId The model ID.
+ * @param serviceId The service ID.
+ * @param client The Hugging Face client.
+ */
public HuggingFaceTextGenerationService(
String modelId,
String serviceId,
@@ -34,6 +43,13 @@ public HuggingFaceTextGenerationService(
this.client = client;
}
+ /**
+ * Get the response to a prompt.
+ * @param prompt The prompt.
+ * @param huggingFacePromptExecutionSettings The settings for executing the prompt.
+ * @param kernel The semantic kernel.
+ * @return The response to the prompt.
+ */
public Mono> getTextContentsAsync(
String prompt,
@Nullable HuggingFacePromptExecutionSettings huggingFacePromptExecutionSettings,
@@ -117,10 +133,17 @@ public String getServiceId() {
return serviceId;
}
+ /**
+ * Create a new builder for HuggingFaceTextGenerationService.
+ * @return The builder.
+ */
public static Builder builder() {
return new Builder();
}
+ /**
+ * A builder for HuggingFaceTextGenerationService.
+ */
public static class Builder {
@Nullable
@@ -152,11 +175,20 @@ public Builder withServiceId(String serviceId) {
return this;
}
+ /**
+ * Sets the HuggingFaceClient for the service
+ * @param client The HuggingFaceClient
+ * @return The builder
+ */
public Builder withHuggingFaceClient(HuggingFaceClient client) {
this.client = client;
return this;
}
+ /**
+ * Builds the HuggingFaceTextGenerationService
+ * @return The HuggingFaceTextGenerationService
+ */
public HuggingFaceTextGenerationService build() {
if (this.modelId == null) {
diff --git a/aiservices/openai/pom.xml b/aiservices/openai/pom.xml
index d6a3762ed..4b46bdd4a 100644
--- a/aiservices/openai/pom.xml
+++ b/aiservices/openai/pom.xml
@@ -6,7 +6,7 @@
com.microsoft.semantic-kernel
semantickernel-parent
- 1.2.2
+ 1.5.1-SNAPSHOT
../../pom.xml
@@ -19,6 +19,30 @@
com.microsoft.semantic-kernel
semantickernel-api
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-data
+ provided
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-exceptions
+ provided
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-ai-services
+ provided
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-builders
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-textembedding-services
+
+
com.azure
azure-ai-openai
@@ -55,6 +79,17 @@
mockito-core
test
+
+
+ com.github.victools
+ jsonschema-generator
+ true
+
+
+ com.github.victools
+ jsonschema-module-jackson
+ true
+
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/OpenAiService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/OpenAiService.java
index 4a7a196b4..0edee4767 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/OpenAiService.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/OpenAiService.java
@@ -6,6 +6,7 @@
/**
* Provides OpenAI service.
+ * @param the client type
*/
public abstract class OpenAiService implements AIService {
@@ -38,10 +39,18 @@ public String getServiceId() {
return serviceId;
}
+ /**
+ * Gets the client.
+ * @return the client
+ */
protected Client getClient() {
return client;
}
+ /**
+ * Gets the deployment name.
+ * @return the deployment name
+ */
public String getDeploymentName() {
return deploymentName;
}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiAudioToTextService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiAudioToTextService.java
index 631f2cac8..7a126f6d8 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiAudioToTextService.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiAudioToTextService.java
@@ -28,6 +28,7 @@ public class OpenAiAudioToTextService extends OpenAiService
*
* @param client OpenAI client.
* @param modelId The model ID.
+ * @param deploymentName The deployment name.
*/
public OpenAiAudioToTextService(
OpenAIAsyncClient client,
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiTextToAudioService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiTextToAudioService.java
index c698fab39..dc748af7d 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiTextToAudioService.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/audio/OpenAiTextToAudioService.java
@@ -27,6 +27,7 @@ public class OpenAiTextToAudioService extends OpenAiService
*
* @param client OpenAI client.
* @param modelId The model ID.
+ * @param deploymentName The deployment name.
*/
public OpenAiTextToAudioService(
OpenAIAsyncClient client,
@@ -79,7 +80,6 @@ public static Builder builder() {
/**
* Represents a builder for OpenAi text to audio service.
*/
-
public static class Builder extends TextToAudioService.Builder {
/**
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/BinaryDataUtils.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/BinaryDataUtils.java
new file mode 100644
index 000000000..9a3189a7e
--- /dev/null
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/BinaryDataUtils.java
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
+
+import com.azure.core.util.BinaryData;
+import javax.annotation.Nullable;
+
+public class BinaryDataUtils {
+
+ @Nullable
+ public static String toString(@Nullable BinaryData b) {
+ if (b == null) {
+ return null;
+ }
+ return b.toString();
+ }
+}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java
index 5442e51ed..8f0ebaf20 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java
@@ -6,11 +6,15 @@
import com.azure.ai.openai.models.ChatCompletions;
import com.azure.ai.openai.models.ChatCompletionsFunctionToolCall;
import com.azure.ai.openai.models.ChatCompletionsFunctionToolDefinition;
+import com.azure.ai.openai.models.ChatCompletionsFunctionToolDefinitionFunction;
import com.azure.ai.openai.models.ChatCompletionsJsonResponseFormat;
+import com.azure.ai.openai.models.ChatCompletionsNamedToolSelection;
import com.azure.ai.openai.models.ChatCompletionsOptions;
import com.azure.ai.openai.models.ChatCompletionsTextResponseFormat;
import com.azure.ai.openai.models.ChatCompletionsToolCall;
import com.azure.ai.openai.models.ChatCompletionsToolDefinition;
+import com.azure.ai.openai.models.ChatCompletionsToolSelection;
+import com.azure.ai.openai.models.ChatCompletionsToolSelectionPreset;
import com.azure.ai.openai.models.ChatMessageImageContentItem;
import com.azure.ai.openai.models.ChatMessageImageDetailLevel;
import com.azure.ai.openai.models.ChatMessageImageUrl;
@@ -23,26 +27,35 @@
import com.azure.ai.openai.models.ChatResponseMessage;
import com.azure.ai.openai.models.CompletionsUsage;
import com.azure.ai.openai.models.FunctionCall;
-import com.azure.core.util.BinaryData;
+import com.azure.ai.openai.models.FunctionDefinition;
+import com.azure.json.JsonOptions;
+import com.azure.json.implementation.DefaultJsonReader;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ContainerNode;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.aiservices.openai.OpenAiService;
+import com.microsoft.semantickernel.aiservices.openai.chatcompletion.responseformat.ChatCompletionsJsonSchemaResponseFormat;
import com.microsoft.semantickernel.aiservices.openai.implementation.OpenAIRequestSettings;
+import com.microsoft.semantickernel.contents.FunctionCallContent;
import com.microsoft.semantickernel.contextvariables.ContextVariable;
import com.microsoft.semantickernel.contextvariables.ContextVariableTypes;
import com.microsoft.semantickernel.exceptions.AIException;
import com.microsoft.semantickernel.exceptions.AIException.ErrorCodes;
import com.microsoft.semantickernel.exceptions.SKCheckedException;
import com.microsoft.semantickernel.exceptions.SKException;
+import com.microsoft.semantickernel.functionchoice.AutoFunctionChoiceBehavior;
+import com.microsoft.semantickernel.functionchoice.FunctionChoiceBehavior;
+import com.microsoft.semantickernel.functionchoice.NoneFunctionChoiceBehavior;
+import com.microsoft.semantickernel.functionchoice.RequiredFunctionChoiceBehavior;
import com.microsoft.semantickernel.hooks.KernelHookEvent;
import com.microsoft.semantickernel.hooks.KernelHooks;
import com.microsoft.semantickernel.hooks.PostChatCompletionEvent;
import com.microsoft.semantickernel.hooks.PreChatCompletionEvent;
import com.microsoft.semantickernel.hooks.PreToolCallEvent;
import com.microsoft.semantickernel.implementation.CollectionUtil;
+import com.microsoft.semantickernel.implementation.telemetry.ChatCompletionSpan;
import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry;
import com.microsoft.semantickernel.orchestration.FunctionResult;
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
@@ -50,20 +63,24 @@
import com.microsoft.semantickernel.orchestration.InvocationReturnMode;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
+import com.microsoft.semantickernel.orchestration.responseformat.JsonResponseSchema;
+import com.microsoft.semantickernel.orchestration.responseformat.JsonSchemaResponseFormat;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
-import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import com.microsoft.semantickernel.services.chatcompletion.AuthorRole;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.services.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent;
+import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent;
import com.microsoft.semantickernel.services.chatcompletion.message.ChatMessageContentType;
import com.microsoft.semantickernel.services.chatcompletion.message.ChatMessageImageContent;
import com.microsoft.semantickernel.services.openai.OpenAiServiceBuilder;
-import io.opentelemetry.api.trace.Span;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -132,7 +149,7 @@ public Mono>> getChatMessageContentsAsync(
if (invocationContext != null
&& invocationContext
- .returnMode() == InvocationReturnMode.LAST_MESSAGE_ONLY) {
+ .returnMode() == InvocationReturnMode.LAST_MESSAGE_ONLY) {
chatHistoryResult = new ChatHistory(
Collections.singletonList(
CollectionUtil.getLastOrNull(chatHistoryResult.getMessages())));
@@ -166,7 +183,7 @@ public Mono>> getChatMessageContentsAsync(
if (invocationContext != null
&& invocationContext
- .returnMode() == InvocationReturnMode.LAST_MESSAGE_ONLY) {
+ .returnMode() == InvocationReturnMode.LAST_MESSAGE_ONLY) {
result = new ChatHistory(
Collections.singletonList(
CollectionUtil.getLastOrNull(result.getMessages())));
@@ -179,6 +196,108 @@ public Mono>> getChatMessageContentsAsync(
});
}
+ @Override
+ public Flux> getStreamingChatMessageContentsAsync(
+ ChatHistory chatHistory,
+ @Nullable Kernel kernel,
+ @Nullable InvocationContext invocationContext) {
+ if (invocationContext != null &&
+ invocationContext.getToolCallBehavior() != null &&
+ invocationContext.getToolCallBehavior().isAutoInvokeAllowed()) {
+ throw new SKException(
+ "ToolCallBehavior auto-invoke is not supported for streaming chat message contents");
+ }
+
+ if (invocationContext != null &&
+ invocationContext.getFunctionChoiceBehavior() != null &&
+ invocationContext.getFunctionChoiceBehavior() instanceof AutoFunctionChoiceBehavior &&
+ ((AutoFunctionChoiceBehavior) invocationContext.getFunctionChoiceBehavior())
+ .isAutoInvoke()) {
+ throw new SKException(
+ "FunctionChoiceBehavior auto-invoke is not supported for streaming chat message contents");
+ }
+
+ if (invocationContext != null
+ && invocationContext.returnMode() != InvocationReturnMode.NEW_MESSAGES_ONLY) {
+ throw new SKException(
+ "Streaming chat message contents only supports NEW_MESSAGES_ONLY return mode");
+ }
+
+ List chatRequestMessages = getChatRequestMessages(chatHistory);
+
+ ChatMessages messages = new ChatMessages(chatRequestMessages);
+
+ List functions = new ArrayList<>();
+ if (kernel != null) {
+ kernel.getPlugins()
+ .forEach(plugin -> plugin.getFunctions().forEach((name, function) -> functions
+ .add(OpenAIFunction.build(function.getMetadata(), plugin.getName()))));
+ }
+
+ OpenAIToolCallConfig toolCallConfig = getToolCallConfig(
+ invocationContext,
+ functions,
+ messages.allMessages,
+ 0);
+
+ ChatCompletionsOptions options = executeHook(
+ invocationContext,
+ kernel,
+ new PreChatCompletionEvent(
+ getCompletionsOptions(
+ this,
+ messages.allMessages,
+ invocationContext,
+ toolCallConfig)))
+ .getOptions();
+
+ return getClient()
+ .getChatCompletionsStreamWithResponse(
+ getDeploymentName(),
+ options,
+ OpenAIRequestSettings.getRequestOptions())
+ .flatMap(completionsResult -> {
+ if (completionsResult.getStatusCode() >= 400) {
+ //SemanticKernelTelemetry.endSpanWithError(span);
+ return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR,
+ "Request failed: " + completionsResult.getStatusCode()));
+ }
+ //SemanticKernelTelemetry.endSpanWithUsage(span, completionsResult.getValue().getUsage());
+
+ return Mono.just(completionsResult.getValue());
+ })
+ .flatMap(completions -> {
+ return Flux.fromIterable(completions.getChoices())
+ .map(message -> {
+ AuthorRole role = message.getDelta().getRole() == null
+ ? AuthorRole.ASSISTANT
+ : AuthorRole.valueOf(message.getDelta().getRole().toString()
+ .toUpperCase(Locale.ROOT));
+
+ return new OpenAIStreamingChatMessageContent<>(
+ completions.getId(),
+ role,
+ message.getDelta().getContent(),
+ getModelId(),
+ null,
+ null,
+ null,
+ Arrays.asList());
+ });
+ });
+ }
+
+ @Override
+ public Flux> getStreamingChatMessageContentsAsync(
+ String prompt,
+ @Nullable Kernel kernel,
+ @Nullable InvocationContext invocationContext) {
+ return getStreamingChatMessageContentsAsync(
+ new ChatHistory().addUserMessage(prompt),
+ kernel,
+ invocationContext);
+ }
+
// Holds messages temporarily as we build up our result
private static class ChatMessages {
@@ -291,16 +410,12 @@ private Mono internalChatMessageContentsAsync(
.add(OpenAIFunction.build(function.getMetadata(), plugin.getName()))));
}
- // Create copy to avoid reactor exceptions when updating request messages internally
return internalChatMessageContentsAsync(
messages,
kernel,
functions,
invocationContext,
- Math.min(MAXIMUM_INFLIGHT_AUTO_INVOKES,
- invocationContext != null && invocationContext.getToolCallBehavior() != null
- ? invocationContext.getToolCallBehavior().getMaximumAutoInvokeAttempts()
- : 0));
+ 0);
}
private Mono internalChatMessageContentsAsync(
@@ -308,7 +423,13 @@ private Mono internalChatMessageContentsAsync(
@Nullable Kernel kernel,
List functions,
@Nullable InvocationContext invocationContext,
- int autoInvokeAttempts) {
+ int requestIndex) {
+
+ OpenAIToolCallConfig toolCallConfig = getToolCallConfig(
+ invocationContext,
+ functions,
+ messages.allMessages,
+ requestIndex);
ChatCompletionsOptions options = executeHook(
invocationContext,
@@ -317,32 +438,37 @@ private Mono internalChatMessageContentsAsync(
getCompletionsOptions(
this,
messages.allMessages,
- functions,
- invocationContext)))
+ invocationContext,
+ toolCallConfig)))
.getOptions();
- Span span = SemanticKernelTelemetry.startChatCompletionSpan(
- getModelId(),
- SemanticKernelTelemetry.OPEN_AI_PROVIDER,
- options.getMaxTokens(),
- options.getTemperature(),
- options.getTopP());
- return getClient()
- .getChatCompletionsWithResponse(getDeploymentName(), options,
- OpenAIRequestSettings.getRequestOptions())
- .flatMap(completionsResult -> {
- if (completionsResult.getStatusCode() >= 400) {
- SemanticKernelTelemetry.endSpanWithError(span);
- return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR,
- "Request failed: " + completionsResult.getStatusCode()));
- }
- SemanticKernelTelemetry.endSpanWithUsage(span,
- completionsResult.getValue().getUsage());
+ return Mono.deferContextual(contextView -> {
+ ChatCompletionSpan span = ChatCompletionSpan.startChatCompletionSpan(
+ SemanticKernelTelemetry.getTelemetry(invocationContext),
+ contextView,
+ getModelId(),
+ SemanticKernelTelemetry.OPEN_AI_PROVIDER,
+ options.getMaxTokens(),
+ options.getTemperature(),
+ options.getTopP());
+
+ return getClient()
+ .getChatCompletionsWithResponse(getDeploymentName(), options,
+ OpenAIRequestSettings.getRequestOptions())
+ .contextWrite(span.getReactorContextModifier())
+ .flatMap(completionsResult -> {
+ if (completionsResult.getStatusCode() >= 400) {
+ return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR,
+ "Request failed: " + completionsResult.getStatusCode()));
+ }
- return Mono.just(completionsResult.getValue());
+ return Mono.just(completionsResult.getValue());
+ })
+ .doOnError(span::endSpanWithError)
+ .doOnSuccess(span::endSpanWithUsage)
+ .doOnTerminate(span::close);
})
.flatMap(completions -> {
-
List responseMessages = completions
.getChoices()
.stream()
@@ -354,13 +480,15 @@ private Mono internalChatMessageContentsAsync(
executeHook(invocationContext, kernel, new PostChatCompletionEvent(completions));
// Just return the result:
- // If we don't want to attempt to invoke any functions
+ // If auto-invoking is not enabled
// Or if we are auto-invoking, but we somehow end up with other than 1 choice even though only 1 was requested
- if (autoInvokeAttempts == 0 || responseMessages.size() != 1) {
+ if (toolCallConfig == null || !toolCallConfig.isAutoInvoke()
+ || responseMessages.size() != 1) {
List> chatMessageContents = getChatMessageContentsAsync(
completions);
return Mono.just(messages.addChatMessage(chatMessageContents));
}
+
// Or if there are no tool calls to be done
ChatResponseMessage response = responseMessages.get(0);
List toolCalls = response.getToolCalls();
@@ -393,14 +521,14 @@ private Mono internalChatMessageContentsAsync(
.flatMap(it -> it)
.flatMap(msgs -> {
return internalChatMessageContentsAsync(msgs, kernel, functions,
- invocationContext, autoInvokeAttempts - 1);
+ invocationContext, requestIndex + 1);
})
.onErrorResume(e -> {
LOGGER.warn("Tool invocation attempt failed: ", e);
// If FunctionInvocationError occurred and there are still attempts left, retry, else exit
- if (autoInvokeAttempts > 0) {
+ if (requestIndex < MAXIMUM_INFLIGHT_AUTO_INVOKES) {
ChatMessages currentMessages = messages;
if (e instanceof FunctionInvocationError) {
currentMessages.assertCommonHistory(
@@ -414,7 +542,7 @@ private Mono internalChatMessageContentsAsync(
kernel,
functions,
invocationContext,
- autoInvokeAttempts - 1);
+ requestIndex + 1);
} else {
return Mono.error(e);
}
@@ -506,8 +634,8 @@ private Mono> invokeFunctionTool(
ContextVariableTypes contextVariableTypes) {
try {
- OpenAIFunctionToolCall openAIFunctionToolCall = extractOpenAIFunctionToolCall(toolCall);
- String pluginName = openAIFunctionToolCall.getPluginName();
+ FunctionCallContent functionCallContent = extractFunctionCallContent(toolCall);
+ String pluginName = functionCallContent.getPluginName();
if (pluginName == null || pluginName.isEmpty()) {
return Mono.error(
new SKException("Plugin name is required for function tool call"));
@@ -515,21 +643,23 @@ private Mono> invokeFunctionTool(
KernelFunction> function = kernel.getFunction(
pluginName,
- openAIFunctionToolCall.getFunctionName());
+ functionCallContent.getFunctionName());
PreToolCallEvent hookResult = executeHook(invocationContext, kernel,
new PreToolCallEvent(
- openAIFunctionToolCall.getFunctionName(),
- openAIFunctionToolCall.getArguments(),
+ functionCallContent.getFunctionName(),
+ functionCallContent.getArguments(),
function,
contextVariableTypes));
function = hookResult.getFunction();
- KernelFunctionArguments arguments = hookResult.getArguments();
+ KernelArguments arguments = hookResult.getArguments();
return function
.invokeAsync(kernel)
.withArguments(arguments)
+ .withTypes(invocationContext.getContextVariableTypes())
+ .withTypes(contextVariableTypes)
.withResultType(contextVariableTypes.getVariableTypeForClass(String.class));
} catch (JsonProcessingException e) {
return Mono.error(new SKException("Failed to parse tool arguments", e));
@@ -557,7 +687,7 @@ private static T executeHook(
}
@SuppressWarnings("StringSplitter")
- private OpenAIFunctionToolCall extractOpenAIFunctionToolCall(
+ private FunctionCallContent extractFunctionCallContent(
ChatCompletionsFunctionToolCall toolCall)
throws JsonProcessingException {
@@ -567,7 +697,7 @@ private OpenAIFunctionToolCall extractOpenAIFunctionToolCall(
String pluginName = parts.length > 1 ? parts[0] : "";
String fnName = parts.length > 1 ? parts[1] : parts[0];
- KernelFunctionArguments arguments = KernelFunctionArguments.builder().build();
+ KernelArguments arguments = KernelArguments.builder().build();
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonToolCallArguments = mapper.readTree(toolCall.getFunction().getArguments());
@@ -583,10 +713,10 @@ private OpenAIFunctionToolCall extractOpenAIFunctionToolCall(
}
});
- return new OpenAIFunctionToolCall(
- toolCall.getId(),
- pluginName,
+ return new FunctionCallContent(
fnName,
+ pluginName,
+ toolCall.getId(),
arguments);
}
@@ -615,7 +745,7 @@ private List> getChatMessageContentsAsync(
null,
null,
completionMetadata,
- formOpenAiToolCalls(response));
+ formFunctionCallContents(response));
} catch (SKCheckedException e) {
LOGGER.warn("Failed to form chat message content", e);
return null;
@@ -636,7 +766,8 @@ private List> toOpenAIChatMessageContent(
if (message instanceof ChatRequestUserMessage) {
return new OpenAIChatMessageContent<>(
AuthorRole.USER,
- ((ChatRequestUserMessage) message).getContent().toString(),
+ BinaryDataUtils
+ .toString(((ChatRequestUserMessage) message).getContent()),
null,
null,
null,
@@ -645,7 +776,8 @@ private List> toOpenAIChatMessageContent(
} else if (message instanceof ChatRequestSystemMessage) {
return new OpenAIChatMessageContent<>(
AuthorRole.SYSTEM,
- ((ChatRequestSystemMessage) message).getContent(),
+ BinaryDataUtils
+ .toString(((ChatRequestSystemMessage) message).getContent()),
null,
null,
null,
@@ -653,11 +785,12 @@ private List> toOpenAIChatMessageContent(
null);
} else if (message instanceof ChatRequestAssistantMessage) {
try {
- List calls = getToolCalls(
+ List calls = getFunctionCallContents(
((ChatRequestAssistantMessage) message).getToolCalls());
return new OpenAIChatMessageContent<>(
AuthorRole.ASSISTANT,
- ((ChatRequestAssistantMessage) message).getContent(),
+ BinaryDataUtils
+ .toString(((ChatRequestAssistantMessage) message).getContent()),
null,
null,
null,
@@ -669,7 +802,8 @@ private List> toOpenAIChatMessageContent(
} else if (message instanceof ChatRequestToolMessage) {
return new OpenAIChatMessageContent<>(
AuthorRole.TOOL,
- ((ChatRequestToolMessage) message).getContent(),
+ BinaryDataUtils
+ .toString(((ChatRequestToolMessage) message).getContent()),
null,
null,
null,
@@ -690,7 +824,7 @@ private List> toOpenAIChatMessageContent(
}
@Nullable
- private List getToolCalls(
+ private List getFunctionCallContents(
@Nullable List toolCalls) throws SKCheckedException {
if (toolCalls == null || toolCalls.isEmpty()) {
return null;
@@ -702,7 +836,7 @@ private List getToolCalls(
.map(call -> {
if (call instanceof ChatCompletionsFunctionToolCall) {
try {
- return extractOpenAIFunctionToolCall(
+ return extractFunctionCallContent(
(ChatCompletionsFunctionToolCall) call);
} catch (JsonProcessingException e) {
throw SKException.build("Failed to parse tool arguments", e);
@@ -719,7 +853,7 @@ private List getToolCalls(
}
@Nullable
- private List formOpenAiToolCalls(
+ private List formFunctionCallContents(
ChatResponseMessage response) throws SKCheckedException {
if (response.getToolCalls() == null || response.getToolCalls().isEmpty()) {
return null;
@@ -731,7 +865,7 @@ private List formOpenAiToolCalls(
.map(call -> {
if (call instanceof ChatCompletionsFunctionToolCall) {
try {
- return extractOpenAIFunctionToolCall(
+ return extractFunctionCallContent(
(ChatCompletionsFunctionToolCall) call);
} catch (JsonProcessingException e) {
throw SKException.build("Failed to parse tool arguments", e);
@@ -750,8 +884,8 @@ private List formOpenAiToolCalls(
private static ChatCompletionsOptions getCompletionsOptions(
ChatCompletionService chatCompletionService,
List chatRequestMessages,
- @Nullable List functions,
- @Nullable InvocationContext invocationContext) {
+ @Nullable InvocationContext invocationContext,
+ @Nullable OpenAIToolCallConfig toolCallConfig) {
chatRequestMessages = chatRequestMessages
.stream()
@@ -761,12 +895,13 @@ private static ChatCompletionsOptions getCompletionsOptions(
ChatCompletionsOptions options = new ChatCompletionsOptions(chatRequestMessages)
.setModel(chatCompletionService.getModelId());
- if (invocationContext != null && invocationContext.getToolCallBehavior() != null) {
- configureToolCallBehaviorOptions(
- options,
- invocationContext.getToolCallBehavior(),
- functions,
- chatRequestMessages);
+ if (toolCallConfig != null) {
+ options.setTools(toolCallConfig.getTools());
+ options.setToolChoice(toolCallConfig.getToolChoice());
+
+ if (toolCallConfig.getOptions() != null) {
+ options.setParallelToolCalls(toolCallConfig.getOptions().isParallelCallsAllowed());
+ }
}
PromptExecutionSettings promptExecutionSettings = invocationContext != null
@@ -785,7 +920,8 @@ private static ChatCompletionsOptions getCompletionsOptions(
}
Map logit = null;
- if (promptExecutionSettings.getTokenSelectionBiases() != null) {
+ if (promptExecutionSettings.getTokenSelectionBiases() != null
+ && !promptExecutionSettings.getTokenSelectionBiases().isEmpty()) {
logit = promptExecutionSettings
.getTokenSelectionBiases()
.entrySet()
@@ -802,23 +938,32 @@ private static ChatCompletionsOptions getCompletionsOptions(
.setFrequencyPenalty(promptExecutionSettings.getFrequencyPenalty())
.setPresencePenalty(promptExecutionSettings.getPresencePenalty())
.setMaxTokens(promptExecutionSettings.getMaxTokens())
+ .setMaxCompletionTokens(promptExecutionSettings.getMaxCompletionTokens())
.setN(promptExecutionSettings.getResultsPerPrompt())
// Azure OpenAI WithData API does not allow to send empty array of stop sequences
// Gives back "Validation error at #/stop/str: Input should be a valid string\nValidation error at #/stop/list[str]: List should have at least 1 item after validation, not 0"
.setStop(promptExecutionSettings.getStopSequences() == null
|| promptExecutionSettings.getStopSequences().isEmpty() ? null
- : promptExecutionSettings.getStopSequences())
+ : promptExecutionSettings.getStopSequences())
.setUser(promptExecutionSettings.getUser())
.setLogitBias(logit);
if (promptExecutionSettings.getResponseFormat() != null) {
- switch (promptExecutionSettings.getResponseFormat()) {
+ switch (promptExecutionSettings.getResponseFormat().getType()) {
+ case JSON_SCHEMA:
+ JsonResponseSchema schema = ((JsonSchemaResponseFormat) promptExecutionSettings
+ .getResponseFormat())
+ .getJsonSchema();
+
+ options.setResponseFormat(new ChatCompletionsJsonSchemaResponseFormat(schema));
+ break;
case JSON_OBJECT:
options.setResponseFormat(new ChatCompletionsJsonResponseFormat());
break;
case TEXT:
options.setResponseFormat(new ChatCompletionsTextResponseFormat());
break;
+
default:
throw new SKException(
"Unknown response format: " + promptExecutionSettings.getResponseFormat());
@@ -828,76 +973,184 @@ private static ChatCompletionsOptions getCompletionsOptions(
return options;
}
- private static void configureToolCallBehaviorOptions(
- ChatCompletionsOptions options,
+ @Nullable
+ private static OpenAIToolCallConfig getToolCallConfig(
+ @Nullable InvocationContext invocationContext,
+ @Nullable List functions,
+ List chatRequestMessages,
+ int requestIndex) {
+
+ if (invocationContext == null || functions == null || functions.isEmpty()) {
+ return null;
+ }
+
+ if (invocationContext.getFunctionChoiceBehavior() == null
+ && invocationContext.getToolCallBehavior() == null) {
+ return null;
+ }
+
+ if (invocationContext.getFunctionChoiceBehavior() != null) {
+ return getFunctionChoiceBehaviorConfig(
+ invocationContext.getFunctionChoiceBehavior(),
+ functions,
+ requestIndex);
+ } else {
+ return getToolCallBehaviorConfig(
+ invocationContext.getToolCallBehavior(),
+ functions,
+ chatRequestMessages,
+ requestIndex);
+ }
+ }
+
+ @Nullable
+ private static OpenAIToolCallConfig getFunctionChoiceBehaviorConfig(
+ @Nullable FunctionChoiceBehavior functionChoiceBehavior,
+ @Nullable List functions,
+ int requestIndex) {
+ if (functionChoiceBehavior == null) {
+ return null;
+ }
+
+ if (functions == null || functions.isEmpty()) {
+ return null;
+ }
+
+ ChatCompletionsToolSelection toolChoice;
+ boolean autoInvoke;
+
+ if (functionChoiceBehavior instanceof RequiredFunctionChoiceBehavior) {
+ // After first request a required function must have been called already
+ if (requestIndex >= 1) {
+ return null;
+ }
+
+ toolChoice = new ChatCompletionsToolSelection(
+ ChatCompletionsToolSelectionPreset.REQUIRED);
+ autoInvoke = ((RequiredFunctionChoiceBehavior) functionChoiceBehavior).isAutoInvoke();
+ } else if (functionChoiceBehavior instanceof AutoFunctionChoiceBehavior) {
+ toolChoice = new ChatCompletionsToolSelection(ChatCompletionsToolSelectionPreset.AUTO);
+ autoInvoke = ((AutoFunctionChoiceBehavior) functionChoiceBehavior).isAutoInvoke()
+ && requestIndex < MAXIMUM_INFLIGHT_AUTO_INVOKES;
+ } else if (functionChoiceBehavior instanceof NoneFunctionChoiceBehavior) {
+ toolChoice = new ChatCompletionsToolSelection(ChatCompletionsToolSelectionPreset.NONE);
+ autoInvoke = false;
+ } else {
+ throw new SKException(
+ "Unsupported function choice behavior: " + functionChoiceBehavior);
+ }
+
+ // List of functions advertised to the model
+ List toolDefinitions = functions.stream()
+ .filter(function -> functionChoiceBehavior.isFunctionAllowed(function.getPluginName(),
+ function.getName()))
+ .map(OpenAIFunction::getFunctionDefinition)
+ .map(it -> new ChatCompletionsFunctionToolDefinitionFunction(it.getName())
+ .setDescription(it.getDescription())
+ .setParameters(it.getParameters()))
+ .map(ChatCompletionsFunctionToolDefinition::new)
+ .collect(Collectors.toList());
+
+ return new OpenAIToolCallConfig(
+ toolDefinitions,
+ toolChoice,
+ autoInvoke,
+ functionChoiceBehavior.getOptions());
+ }
+
+ @Nullable
+ private static OpenAIToolCallConfig getToolCallBehaviorConfig(
@Nullable ToolCallBehavior toolCallBehavior,
@Nullable List functions,
- List chatRequestMessages) {
+ List chatRequestMessages,
+ int requestIndex) {
if (toolCallBehavior == null) {
- return;
+ return null;
}
if (functions == null || functions.isEmpty()) {
- return;
+ return null;
}
+ List toolDefinitions;
+ ChatCompletionsToolSelection toolChoice;
+
// If a specific function is required to be called
if (toolCallBehavior instanceof ToolCallBehavior.RequiredKernelFunction) {
- KernelFunction> toolChoice = ((ToolCallBehavior.RequiredKernelFunction) toolCallBehavior)
+ KernelFunction> requiredFunction = ((ToolCallBehavior.RequiredKernelFunction) toolCallBehavior)
.getRequiredFunction();
String toolChoiceName = String.format("%s%s%s",
- toolChoice.getPluginName(),
+ requiredFunction.getPluginName(),
OpenAIFunction.getNameSeparator(),
- toolChoice.getName());
+ requiredFunction.getName());
// If required tool call has already been called dont ask for it again
boolean hasBeenExecuted = hasToolCallBeenExecuted(chatRequestMessages, toolChoiceName);
if (hasBeenExecuted) {
- return;
+ return null;
}
- List toolDefinitions = new ArrayList<>();
+ FunctionDefinition function = OpenAIFunction.toFunctionDefinition(
+ requiredFunction.getMetadata(),
+ requiredFunction.getPluginName());
+ toolDefinitions = new ArrayList<>();
toolDefinitions.add(new ChatCompletionsFunctionToolDefinition(
- OpenAIFunction.toFunctionDefinition(
- toolChoice.getMetadata(),
- toolChoice.getPluginName())));
+ new ChatCompletionsFunctionToolDefinitionFunction(function.getName())
+ .setDescription(function.getDescription())
+ .setParameters(function.getParameters())));
- options.setTools(toolDefinitions);
try {
String json = String.format(
"{\"type\":\"function\",\"function\":{\"name\":\"%s\"}}", toolChoiceName);
- options.setToolChoice(BinaryData.fromObject(new ObjectMapper().readTree(json)));
+
+ toolChoice = new ChatCompletionsToolSelection(
+ ChatCompletionsNamedToolSelection.fromJson(
+ DefaultJsonReader.fromString(
+ json,
+ new JsonOptions())));
} catch (JsonProcessingException e) {
throw SKException.build("Failed to parse tool choice", e);
+ } catch (IOException e) {
+ throw new SKException(e);
}
- return;
}
-
// If a set of functions are enabled to be called
- ToolCallBehavior.AllowedKernelFunctions enabledKernelFunctions = (ToolCallBehavior.AllowedKernelFunctions) toolCallBehavior;
- List toolDefinitions = functions.stream()
- .filter(function -> {
- // check if all kernel functions are enabled
- if (enabledKernelFunctions.isAllKernelFunctionsAllowed()) {
- return true;
- }
- // otherwise, check for the specific function
- return enabledKernelFunctions.isFunctionAllowed(function.getPluginName(),
- function.getName());
- })
- .map(OpenAIFunction::getFunctionDefinition)
- .map(ChatCompletionsFunctionToolDefinition::new)
- .collect(Collectors.toList());
+ else {
+ toolChoice = new ChatCompletionsToolSelection(ChatCompletionsToolSelectionPreset.AUTO);
+
+ ToolCallBehavior.AllowedKernelFunctions enabledKernelFunctions = (ToolCallBehavior.AllowedKernelFunctions) toolCallBehavior;
+ toolDefinitions = functions.stream()
+ .filter(function -> {
+ // check if all kernel functions are enabled
+ if (enabledKernelFunctions.isAllKernelFunctionsAllowed()) {
+ return true;
+ }
+ // otherwise, check for the specific function
+ return enabledKernelFunctions.isFunctionAllowed(function.getPluginName(),
+ function.getName());
+ })
+ .map(OpenAIFunction::getFunctionDefinition)
+ .map(it -> new ChatCompletionsFunctionToolDefinitionFunction(it.getName())
+ .setDescription(it.getDescription())
+ .setParameters(it.getParameters()))
+ .map(ChatCompletionsFunctionToolDefinition::new)
+ .collect(Collectors.toList());
- if (toolDefinitions.isEmpty()) {
- return;
+ if (toolDefinitions.isEmpty()) {
+ return null;
+ }
}
- options.setTools(toolDefinitions);
- options.setToolChoice(BinaryData.fromString("auto"));
+ return new OpenAIToolCallConfig(
+ toolDefinitions,
+ toolChoice,
+ toolCallBehavior.isAutoInvokeAllowed()
+ && requestIndex < Math.min(MAXIMUM_INFLIGHT_AUTO_INVOKES,
+ toolCallBehavior.getMaximumAutoInvokeAttempts()),
+ null);
}
private static boolean hasToolCallBeenExecuted(List chatRequestMessages,
@@ -1001,24 +1254,21 @@ private static ChatRequestAssistantMessage formAssistantMessage(
// TODO: handle tools other than function calls
ChatRequestAssistantMessage asstMessage = new ChatRequestAssistantMessage(content);
- List toolCalls = null;
- if (message instanceof OpenAIChatMessageContent) {
- toolCalls = ((OpenAIChatMessageContent>) message).getToolCall();
- }
+ List toolCalls = FunctionCallContent.getFunctionCalls(message);
if (toolCalls != null) {
asstMessage.setToolCalls(
toolCalls.stream()
.map(toolCall -> {
- KernelFunctionArguments arguments = toolCall.getArguments();
+ KernelArguments arguments = toolCall.getArguments();
String args = arguments != null && !arguments.isEmpty()
? arguments.entrySet().stream()
- .map(entry -> String.format("\"%s\": \"%s\"",
- StringEscapeUtils.escapeJson(entry.getKey()),
- StringEscapeUtils.escapeJson(
- entry.getValue().toPromptString())))
- .collect(Collectors.joining(",", "{", "}"))
+ .map(entry -> String.format("\"%s\": \"%s\"",
+ StringEscapeUtils.escapeJson(entry.getKey()),
+ StringEscapeUtils.escapeJson(
+ entry.getValue().toPromptString())))
+ .collect(Collectors.joining(",", "{", "}"))
: "{}";
String prefix = "";
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java
index 89f45014a..ed1e28329 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java
@@ -1,12 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
+import com.microsoft.semantickernel.contents.FunctionCallContent;
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
+import com.microsoft.semantickernel.services.KernelContent;
import com.microsoft.semantickernel.services.chatcompletion.AuthorRole;
import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
@@ -16,6 +19,7 @@
*/
public class OpenAIChatMessageContent extends ChatMessageContent {
+ @Deprecated
@Nullable
private final List toolCall;
@@ -28,7 +32,7 @@ public class OpenAIChatMessageContent extends ChatMessageContent {
* @param innerContent The inner content.
* @param encoding The encoding.
* @param metadata The metadata.
- * @param toolCall The tool call.
+ * @param functionCalls The tool call.
*/
public OpenAIChatMessageContent(
AuthorRole authorRole,
@@ -37,13 +41,25 @@ public OpenAIChatMessageContent(
@Nullable T innerContent,
@Nullable Charset encoding,
@Nullable FunctionResultMetadata> metadata,
- @Nullable List toolCall) {
- super(authorRole, content, modelId, innerContent, encoding, metadata);
+ @Nullable List extends FunctionCallContent> functionCalls) {
+ super(authorRole, content, (List extends KernelContent>) functionCalls, modelId,
+ innerContent, encoding, metadata);
- if (toolCall == null) {
+ if (functionCalls == null) {
this.toolCall = null;
} else {
- this.toolCall = Collections.unmodifiableList(toolCall);
+ // Keep OpenAIFunctionToolCall list for legacy
+ this.toolCall = Collections.unmodifiableList(functionCalls.stream().map(t -> {
+ if (t instanceof OpenAIFunctionToolCall) {
+ return (OpenAIFunctionToolCall) t;
+ } else {
+ return new OpenAIFunctionToolCall(
+ t.getId(),
+ t.getPluginName(),
+ t.getFunctionName(),
+ t.getArguments());
+ }
+ }).collect(Collectors.toList()));
}
}
@@ -51,7 +67,10 @@ public OpenAIChatMessageContent(
* Gets any tool calls requested.
*
* @return The tool call.
+ *
+ * @deprecated Use {@link FunctionCallContent#getFunctionCalls(ChatMessageContent)} instead.
*/
+ @Deprecated
@Nullable
public List getToolCall() {
return toolCall;
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunction.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunction.java
index 0ed048e8a..4ef550c23 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunction.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunction.java
@@ -7,16 +7,20 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.microsoft.semantickernel.orchestration.responseformat.ResponseSchemaGenerator;
+import com.microsoft.semantickernel.plugin.KernelPluginFactory;
import com.microsoft.semantickernel.semanticfunctions.InputVariable;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionMetadata;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
class OpenAIFunction {
@@ -149,17 +153,26 @@ public Map getProperties() {
private static String getSchemaForFunctionParameter(@Nullable InputVariable parameter) {
List entries = new ArrayList<>();
- entries.add("\"type\":\"string\"");
+ String type = "string";
+
+ if (parameter != null && parameter.getType() != null) {
+ type = getJavaTypeToOpenAiFunctionType(parameter.getType());
+ }
+
+ entries.add("\"type\":\"" + type + "\"");
// Add description if present
+ String description = null;
if (parameter != null && parameter.getDescription() != null && !parameter.getDescription()
.isEmpty()) {
- String description = parameter.getDescription();
+ description = parameter.getDescription();
description = description.replaceAll("\\r?\\n|\\r", "");
description = description.replace("\"", "\\\"");
-
- description = String.format("\"description\":\"%s\"", description);
- entries.add(description);
+ entries.add(String.format("\"description\":\"%s\"", description));
+ }
+ // If custom type, generate schema
+ if ("object".equalsIgnoreCase(type)) {
+ return getObjectSchema(parameter.getType(), description);
}
// Add enum options if parameter is an enum
@@ -179,4 +192,48 @@ private static String getSchemaForFunctionParameter(@Nullable InputVariable para
return "{" + schema + "}";
}
+
+ private static String getJavaTypeToOpenAiFunctionType(String javaType) {
+ switch (javaType.toLowerCase(Locale.ROOT)) {
+ case "java.lang.boolean":
+ case "boolean":
+ return "boolean";
+ case "java.lang.integer":
+ case "integer":
+ case "int":
+ case "java.lang.long":
+ case "long":
+ case "java.lang.short":
+ case "short":
+ case "java.lang.byte":
+ case "byte":
+ return "integer";
+ case "java.lang.double":
+ case "double":
+ case "java.lang.float":
+ case "float":
+ return "number";
+ case "java.lang.string":
+ case "string":
+ return "string";
+ case "array":
+ return "array";
+ case "java.lang.void":
+ case "void":
+ return "null";
+ default:
+ return "object";
+ }
+ }
+
+ private static String getObjectSchema(String type, String description) {
+ Class> clazz = KernelPluginFactory.getTypeForName(type);
+ String schema = ResponseSchemaGenerator.jacksonGenerator().generateSchema(clazz);
+
+ Map properties = BinaryData.fromString(schema).toObject(Map.class);
+ if (StringUtils.isNotBlank(description)) {
+ properties.put("description", description);
+ }
+ return BinaryData.fromObject(properties).toString();
+ }
}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java
index 18111330d..c1def3379 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java
@@ -1,29 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
-import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
+import com.microsoft.semantickernel.contents.FunctionCallContent;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
import javax.annotation.Nullable;
/**
* Represents a call to a function in the OpenAI tool.
+ *
+ * @deprecated Use {@link FunctionCallContent} instead.
*/
-public class OpenAIFunctionToolCall {
-
- /// Gets the ID of the tool call.
- @Nullable
- private final String id;
-
- /// Gets the name of the plugin with which this function is associated, if any.
-
- @Nullable
- private final String pluginName;
-
- /// Gets the name of the function.
- private final String functionName;
-
- /// Gets a name/value collection of the arguments to the function, if any.
- @Nullable
- private final KernelFunctionArguments arguments;
+@Deprecated
+public class OpenAIFunctionToolCall extends FunctionCallContent {
/**
* Creates a new instance of the {@link OpenAIFunctionToolCall} class.
@@ -37,56 +25,7 @@ public OpenAIFunctionToolCall(
@Nullable String id,
@Nullable String pluginName,
String functionName,
- @Nullable KernelFunctionArguments arguments) {
- this.id = id;
- this.pluginName = pluginName;
- this.functionName = functionName;
- if (arguments == null) {
- this.arguments = null;
- } else {
- this.arguments = arguments.copy();
- }
- }
-
- /**
- * Gets the ID of the tool call.
- *
- * @return The ID of the tool call.
- */
- @Nullable
- public String getId() {
- return id;
- }
-
- /**
- * Gets the name of the plugin with which this function is associated, if any.
- *
- * @return The name of the plugin with which this function is associated, if any.
- */
- @Nullable
- public String getPluginName() {
- return pluginName;
- }
-
- /**
- * Gets the name of the function.
- *
- * @return The name of the function.
- */
- public String getFunctionName() {
- return functionName;
- }
-
- /**
- * Gets a name/value collection of the arguments to the function, if any.
- *
- * @return A name/value collection of the arguments to the function, if any.
- */
- @Nullable
- public KernelFunctionArguments getArguments() {
- if (arguments == null) {
- return null;
- }
- return arguments.copy();
+ @Nullable KernelArguments arguments) {
+ super(functionName, pluginName, id, arguments);
}
}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIStreamingChatMessageContent.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIStreamingChatMessageContent.java
new file mode 100644
index 000000000..c919f5c6e
--- /dev/null
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIStreamingChatMessageContent.java
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
+
+import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
+import com.microsoft.semantickernel.services.chatcompletion.AuthorRole;
+import com.microsoft.semantickernel.services.chatcompletion.StreamingChatContent;
+import java.nio.charset.Charset;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Represents the content of a chat message.
+ *
+ * @param The type of the inner content.
+ */
+public class OpenAIStreamingChatMessageContent extends OpenAIChatMessageContent implements
+ StreamingChatContent {
+
+ private final String id;
+
+ /**
+ * Creates a new instance of the {@link OpenAIChatMessageContent} class.
+ *
+ * @param id The id of the message.
+ * @param authorRole The author role that generated the content.
+ * @param content The content.
+ * @param modelId The model id.
+ * @param innerContent The inner content.
+ * @param encoding The encoding.
+ * @param metadata The metadata.
+ * @param toolCall The tool call.
+ */
+ public OpenAIStreamingChatMessageContent(
+ String id,
+ AuthorRole authorRole,
+ String content,
+ @Nullable String modelId,
+ @Nullable T innerContent,
+ @Nullable Charset encoding,
+ @Nullable FunctionResultMetadata metadata,
+ @Nullable List toolCall) {
+ super(
+ authorRole,
+ content,
+ modelId,
+ innerContent,
+ encoding,
+ metadata,
+ toolCall);
+
+ this.id = id;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIToolCallConfig.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIToolCallConfig.java
new file mode 100644
index 000000000..454ed3ce1
--- /dev/null
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIToolCallConfig.java
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
+
+import com.azure.ai.openai.models.ChatCompletionsToolDefinition;
+import com.azure.ai.openai.models.ChatCompletionsToolSelection;
+import com.microsoft.semantickernel.functionchoice.FunctionChoiceBehaviorOptions;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+
+public class OpenAIToolCallConfig {
+ private final List tools;
+ private final ChatCompletionsToolSelection toolChoice;
+ private final boolean autoInvoke;
+ @Nullable
+ private final FunctionChoiceBehaviorOptions options;
+
+ /**
+ * Creates a new instance of the {@link OpenAIToolCallConfig} class.
+ *
+ * @param tools The list of tools available for the call.
+ * @param toolChoice The tool selection strategy.
+ * @param autoInvoke Indicates whether to automatically invoke the tool.
+ * @param options Additional options for function choice behavior.
+ */
+ @SuppressFBWarnings("EI_EXPOSE_REP2")
+ public OpenAIToolCallConfig(
+ List tools,
+ ChatCompletionsToolSelection toolChoice,
+ boolean autoInvoke,
+ @Nullable FunctionChoiceBehaviorOptions options) {
+ this.tools = tools;
+ this.toolChoice = toolChoice;
+ this.autoInvoke = autoInvoke;
+ this.options = options;
+ }
+
+ /**
+ * Gets the list of tools available for the call.
+ *
+ * @return The list of tools.
+ */
+ public List getTools() {
+ return Collections.unmodifiableList(tools);
+ }
+
+ /**
+ * Gets the tool selection strategy.
+ *
+ * @return The tool selection strategy.
+ */
+ public ChatCompletionsToolSelection getToolChoice() {
+ return toolChoice;
+ }
+
+ /**
+ * Indicates whether to automatically invoke the tool.
+ *
+ * @return True if auto-invocation is enabled; otherwise, false.
+ */
+ public boolean isAutoInvoke() {
+ return autoInvoke;
+ }
+
+ /**
+ * Gets additional options for function choice behavior.
+ *
+ * @return The function choice behavior options.
+ */
+ public FunctionChoiceBehaviorOptions getOptions() {
+ return options;
+ }
+}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiXMLPromptParser.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiXMLPromptParser.java
index 4088f72f5..4f5fd99cc 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiXMLPromptParser.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiXMLPromptParser.java
@@ -118,19 +118,23 @@ public static ChatRequestMessage unescapeRequest(ChatRequestMessage message) {
if (message instanceof ChatRequestUserMessage) {
ChatRequestUserMessage chatRequestMessage = (ChatRequestUserMessage) message;
String content = StringEscapeUtils.unescapeXml(
- chatRequestMessage.getContent().toString());
+ BinaryDataUtils.toString(chatRequestMessage.getContent()));
return new ChatRequestUserMessage(content)
.setName(chatRequestMessage.getName());
} else if (message instanceof ChatRequestSystemMessage) {
ChatRequestSystemMessage chatRequestMessage = (ChatRequestSystemMessage) message;
- String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent());
+ String content = StringEscapeUtils
+ .unescapeXml(
+ BinaryDataUtils.toString(chatRequestMessage.getContent()));
return new ChatRequestSystemMessage(content)
.setName(chatRequestMessage.getName());
} else if (message instanceof ChatRequestAssistantMessage) {
ChatRequestAssistantMessage chatRequestMessage = (ChatRequestAssistantMessage) message;
- String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent());
+ String content = StringEscapeUtils
+ .unescapeXml(
+ BinaryDataUtils.toString(chatRequestMessage.getContent()));
return new ChatRequestAssistantMessage(content)
.setToolCalls(chatRequestMessage.getToolCalls())
@@ -145,7 +149,9 @@ public static ChatRequestMessage unescapeRequest(ChatRequestMessage message) {
content);
} else if (message instanceof ChatRequestToolMessage) {
ChatRequestToolMessage chatRequestMessage = (ChatRequestToolMessage) message;
- String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent());
+ String content = StringEscapeUtils
+ .unescapeXml(
+ BinaryDataUtils.toString(chatRequestMessage.getContent()));
return new ChatRequestToolMessage(
content,
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/ChatCompletionsJsonSchemaResponseFormat.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/ChatCompletionsJsonSchemaResponseFormat.java
new file mode 100644
index 000000000..f5a3b1c4f
--- /dev/null
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/ChatCompletionsJsonSchemaResponseFormat.java
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion.responseformat;
+
+import com.azure.ai.openai.models.ChatCompletionsResponseFormat;
+import com.azure.json.JsonWriter;
+import com.microsoft.semantickernel.orchestration.responseformat.JsonResponseSchema;
+import java.io.IOException;
+
+/**
+ * Represents a response format for chat completions that uses a JSON schema.
+ */
+public class ChatCompletionsJsonSchemaResponseFormat extends ChatCompletionsResponseFormat {
+
+ private final JsonResponseSchema schema;
+ private String type = "json_schema";
+
+ /**
+ * Creates a new instance of the {@link ChatCompletionsJsonSchemaResponseFormat} class.
+ *
+ * @param schema The JSON schema.
+ */
+ public ChatCompletionsJsonSchemaResponseFormat(JsonResponseSchema schema) {
+ this.schema = schema;
+ }
+
+ @Override
+ public String getType() {
+ return this.type;
+ }
+
+ @Override
+ public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
+ jsonWriter.writeStartObject();
+ jsonWriter.writeStringField("type", this.type);
+ jsonWriter.writeStartObject("json_schema");
+
+ jsonWriter.writeBooleanField("strict", this.schema.isStrict());
+ jsonWriter.writeStringField("name", this.schema.getName());
+
+ jsonWriter.writeRawField("schema", this.schema.getSchema());
+ jsonWriter.writeEndObject();
+ return jsonWriter.writeEndObject();
+ }
+
+}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/JacksonResponseFormatGenerator.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/JacksonResponseFormatGenerator.java
new file mode 100644
index 000000000..97060cab0
--- /dev/null
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/responseformat/JacksonResponseFormatGenerator.java
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion.responseformat;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ContainerNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.OptionPreset;
+import com.github.victools.jsonschema.generator.SchemaGenerator;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import com.github.victools.jsonschema.generator.SchemaVersion;
+import com.github.victools.jsonschema.module.jackson.JacksonModule;
+import com.microsoft.semantickernel.orchestration.responseformat.ResponseSchemaGenerator;
+
+/**
+ * Represents a response format generator that uses Jackson.
+ */
+public class JacksonResponseFormatGenerator implements ResponseSchemaGenerator {
+
+ private final SchemaGenerator generator;
+
+ /**
+ * Creates a new instance of the {@link JacksonResponseFormatGenerator} class.
+ */
+ public JacksonResponseFormatGenerator() {
+ JacksonModule module = new JacksonModule();
+ SchemaGeneratorConfigBuilder builder = new SchemaGeneratorConfigBuilder(
+ SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
+ .with(module);
+
+ builder
+ .forFields()
+ .withRequiredCheck(fieldScope -> {
+ return true;
+ });
+
+ generator = new SchemaGenerator(builder.build());
+ }
+
+ /**
+ * Creates a new instance of the {@link JacksonResponseFormatGenerator} class.
+ *
+ * @param generator The schema generator.
+ */
+ public JacksonResponseFormatGenerator(SchemaGenerator generator) {
+ this.generator = generator;
+ }
+
+ @Override
+ public String generateSchema(Class> clazz) {
+ ObjectNode schema = generator.generateSchema(clazz);
+
+ sanitize(schema);
+
+ return schema.toPrettyString();
+ }
+
+ private static void sanitize(ContainerNode schema) {
+ if (schema instanceof ObjectNode) {
+ ((ObjectNode) schema).remove("$schema");
+
+ if (schema.has("type") && schema.get("type").asText().equals("object")) {
+ ((ObjectNode) schema).put("additionalProperties", false);
+ }
+
+ for (JsonNode node : (ObjectNode) schema) {
+ if (node instanceof ContainerNode) {
+ sanitize((ContainerNode) node);
+ }
+ }
+ } else if (schema instanceof ArrayNode) {
+ for (JsonNode node : (ArrayNode) schema) {
+ if (node instanceof ContainerNode) {
+ sanitize((ContainerNode) node);
+ }
+ }
+ }
+ }
+}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAIStreamingTextContent.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAIStreamingTextContent.java
new file mode 100644
index 000000000..13272fb50
--- /dev/null
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAIStreamingTextContent.java
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.textcompletion;
+
+import com.microsoft.semantickernel.services.StreamingTextContent;
+import com.microsoft.semantickernel.services.textcompletion.TextContent;
+
+import javax.annotation.Nullable;
+
+/**
+ * StreamingTextContent is a wrapper for TextContent that allows for streaming.
+ */
+public class OpenAIStreamingTextContent extends StreamingTextContent {
+
+ /**
+ * Initializes a new instance of the {@code StreamingTextContent} class with a provided text
+ * content.
+ *
+ * @param content The text content.
+ */
+ public OpenAIStreamingTextContent(TextContent content) {
+ super(content, 0, null, null);
+ }
+
+ @Override
+ @Nullable
+ public String getContent() {
+ TextContent content = getInnerContent();
+ if (content == null) {
+ return null;
+ }
+ return content.getContent();
+ }
+
+}
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java
index 13783229d..ec04c568d 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java
@@ -11,11 +11,9 @@
import com.microsoft.semantickernel.exceptions.AIException.ErrorCodes;
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
-import com.microsoft.semantickernel.services.textcompletion.StreamingTextContent;
+import com.microsoft.semantickernel.services.StreamingTextContent;
import com.microsoft.semantickernel.services.textcompletion.TextContent;
import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
-import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry;
-import io.opentelemetry.api.trace.Span;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -75,7 +73,7 @@ public Flux getStreamingTextContentsAsync(
return this
.internalCompleteTextAsync(prompt, executionSettings)
.flatMapMany(it -> Flux.fromStream(it.stream())
- .map(StreamingTextContent::new));
+ .map(OpenAIStreamingTextContent::new));
}
protected Mono> internalCompleteTextAsync(
@@ -84,23 +82,14 @@ protected Mono> internalCompleteTextAsync(
CompletionsOptions completionsOptions = getCompletionsOptions(text, requestSettings);
- Span span = SemanticKernelTelemetry.startTextCompletionSpan(
- getModelId(),
- SemanticKernelTelemetry.OPEN_AI_PROVIDER,
- completionsOptions.getMaxTokens(),
- completionsOptions.getTemperature(),
- completionsOptions.getTopP());
return getClient()
.getCompletionsWithResponse(getDeploymentName(), completionsOptions,
OpenAIRequestSettings.getRequestOptions())
.flatMap(completionsResult -> {
if (completionsResult.getStatusCode() >= 400) {
- SemanticKernelTelemetry.endSpanWithError(span);
return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR,
"Request failed: " + completionsResult.getStatusCode()));
}
- SemanticKernelTelemetry.endSpanWithUsage(span,
- completionsResult.getValue().getUsage());
return Mono.just(completionsResult.getValue());
})
.map(completions -> {
diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textembedding/OpenAITextEmbeddingGenerationService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textembedding/OpenAITextEmbeddingGenerationService.java
index cd2c7aa89..355f2ab95 100644
--- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textembedding/OpenAITextEmbeddingGenerationService.java
+++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textembedding/OpenAITextEmbeddingGenerationService.java
@@ -8,35 +8,45 @@
import com.microsoft.semantickernel.aiservices.openai.OpenAiService;
import com.microsoft.semantickernel.exceptions.AIException;
import com.microsoft.semantickernel.services.openai.OpenAiServiceBuilder;
-import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
import com.microsoft.semantickernel.services.textembedding.Embedding;
import com.microsoft.semantickernel.services.textembedding.TextEmbeddingGenerationService;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
-import javax.annotation.Nullable;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* An OpenAI implementation of a {@link TextEmbeddingGenerationService}.
- *
*/
public class OpenAITextEmbeddingGenerationService extends OpenAiService
implements TextEmbeddingGenerationService {
+
private static final Logger LOGGER = LoggerFactory
.getLogger(OpenAITextEmbeddingGenerationService.class);
- private static final int DEFAULT_DIMENSIONS = 1536;
private final int dimensions;
+ /**
+ * Dimension of the OpenAI
+ * {@code text-embedding-3-small} model.
+ */
+ public static final int EMBEDDING_DIMENSIONS_SMALL = 1536;
+ /**
+ * Dimension of the OpenAI
+ * {@code text-embedding-3-large} model.
+ */
+ public static final int EMBEDDING_DIMENSIONS_LARGE = 3072;
+
/**
* Creates a new {@link OpenAITextEmbeddingGenerationService}.
*
- * @param client OpenAI client
+ * @param client OpenAI client
* @param deploymentName deployment name
- * @param modelId OpenAI model id
- * @param serviceId Service id
+ * @param dimensions The dimensions for the embeddings.
+ * @param modelId OpenAI model id
+ * @param serviceId Service id
*/
public OpenAITextEmbeddingGenerationService(
OpenAIAsyncClient client,
@@ -57,6 +67,24 @@ public static Builder builder() {
return new Builder();
}
+ /**
+ * Generates embeddings for the given data.
+ *
+ * @param data The data to generate embeddings for.
+ * @return A Mono that completes with the embeddings.
+ */
+ @Override
+ public Mono generateEmbeddingAsync(String data) {
+ return this.internalGenerateTextEmbeddingsAsync(Arrays.asList(data))
+ .flatMap(embeddings -> {
+ if (embeddings.isEmpty()) {
+ return Mono.empty();
+ }
+
+ return Mono.just(embeddings.get(0));
+ });
+ }
+
/**
* Generates embeddings for the given data.
*
@@ -71,8 +99,10 @@ public Mono> generateEmbeddingsAsync(List data) {
protected Mono> internalGenerateTextEmbeddingsAsync(List data) {
EmbeddingsOptions options = new EmbeddingsOptions(data)
.setModel(getModelId())
- .setDimensions(dimensions)
.setInputType("string");
+ if (dimensions > 0) {
+ options.setDimensions(dimensions);
+ }
return getClient()
.getEmbeddings(getModelId(), options)
@@ -88,7 +118,8 @@ protected Mono> internalGenerateTextEmbeddingsAsync(List
*/
public static class Builder extends
OpenAiServiceBuilder {
- private int dimensions = DEFAULT_DIMENSIONS;
+
+ private int dimensions = -1;
/**
* Sets the dimensions for the embeddings.
diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java
index c8bde155b..5c3444b3a 100644
--- a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java
+++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java
@@ -75,57 +75,6 @@ public static void shutdown() {
otel.shutdown();
}
- @Test
- public void otelTextCaptureTest() {
-
- OpenAIAsyncClient openAIAsyncClient = Mockito.mock(OpenAIAsyncClient.class);
-
- CompletionsUsage completionsUsage = Mockito.mock(CompletionsUsage.class);
- Mockito.when(completionsUsage.getCompletionTokens()).thenReturn(22);
- Mockito.when(completionsUsage.getPromptTokens()).thenReturn(55);
-
- Completions completions = Mockito.mock(Completions.class);
- Mockito.when(completions.getUsage()).thenReturn(completionsUsage);
-
- Response response = Mockito.mock(Response.class);
- Mockito.when(response.getStatusCode()).thenReturn(200);
- Mockito.when(response.getValue()).thenReturn(completions);
-
- Mockito.when(openAIAsyncClient.getCompletionsWithResponse(
- Mockito.any(),
- Mockito.any(),
- Mockito.any())).thenAnswer(invocation -> Mono.just(response));
-
- TextGenerationService client = OpenAITextGenerationService.builder()
- .withOpenAIAsyncClient(openAIAsyncClient)
- .withModelId("a-model")
- .build();
-
- try {
- client.getTextContentsAsync(
- "foo",
- null,
- null).block();
- } catch (Exception e) {
- // Expect to fail
- }
-
- Assertions.assertFalse(spans.isEmpty());
- Assertions.assertEquals("a-model",
- spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.request.model")));
- Assertions.assertEquals("text.completions",
- spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name")));
- Assertions.assertEquals("openai",
- spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.system")));
- Assertions.assertEquals(22,
- spans.get(0).getAttributes()
- .get(AttributeKey.longKey("gen_ai.response.completion_tokens")));
- Assertions.assertEquals(55,
- spans.get(0).getAttributes()
- .get(AttributeKey.longKey("gen_ai.response.prompt_tokens")));
-
- }
-
@Test
public void otelChatCaptureTest() {
OpenAIAsyncClient openAIAsyncClient = Mockito.mock(OpenAIAsyncClient.class);
@@ -169,9 +118,9 @@ public void otelChatCaptureTest() {
spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.system")));
Assertions.assertEquals(21,
spans.get(0).getAttributes()
- .get(AttributeKey.longKey("gen_ai.response.completion_tokens")));
+ .get(AttributeKey.longKey("gen_ai.usage.output_tokens")));
Assertions.assertEquals(42,
spans.get(0).getAttributes()
- .get(AttributeKey.longKey("gen_ai.response.prompt_tokens")));
+ .get(AttributeKey.longKey("gen_ai.usage.input_tokens")));
}
}
diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Bar.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Bar.java
new file mode 100644
index 000000000..45d1caa78
--- /dev/null
+++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Bar.java
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class Bar {
+
+ private final String bar;
+
+ public Bar(
+ @JsonProperty("bar") String bar) {
+ this.bar = bar;
+ }
+
+ public String getBar() {
+ return bar;
+ }
+
+}
diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Baz.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Baz.java
new file mode 100644
index 000000000..90ad60ac1
--- /dev/null
+++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Baz.java
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class Baz {
+
+ @JsonProperty("bar")
+ private final Bar bar;
+
+ @JsonCreator
+ public Baz(
+ @JsonProperty("bar") Bar bar) {
+ this.bar = bar;
+ }
+
+ @JsonProperty("bar")
+ public Bar getBar() {
+ return bar;
+ }
+}
\ No newline at end of file
diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Foo.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Foo.java
new file mode 100644
index 000000000..34479b221
--- /dev/null
+++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/Foo.java
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class Foo {
+
+ @JsonProperty("bar")
+ private final T bar;
+
+ @JsonCreator
+ public Foo(
+ @JsonProperty("bar") T bar) {
+ this.bar = bar;
+ }
+
+ @JsonProperty("bar")
+ public T getBar() {
+ return bar;
+ }
+}
\ No newline at end of file
diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/JsonSchemaTest.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/JsonSchemaTest.java
new file mode 100644
index 000000000..33870fba2
--- /dev/null
+++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/JsonSchemaTest.java
@@ -0,0 +1,112 @@
+// Copyright (c) Microsoft. All rights reserved.
+package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
+
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.microsoft.semantickernel.orchestration.responseformat.JsonSchemaResponseFormat;
+import com.microsoft.semantickernel.plugin.KernelPlugin;
+import com.microsoft.semantickernel.plugin.KernelPluginFactory;
+import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
+import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
+import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+
+public class JsonSchemaTest {
+
+ @Test
+ public void jacksonGenerationTest() throws JsonProcessingException {
+ JsonSchemaResponseFormat format = JsonSchemaResponseFormat.builder()
+ .setResponseFormat(Foo.class)
+ .setName("foo")
+ .build();
+
+ Assertions.assertEquals("foo", format.getJsonSchema().getName());
+
+ Assertions.assertTrue(format.getJsonSchema().getSchema()
+ .replaceAll("\\r\\n|\\r|\\n", "")
+ .replaceAll(" +", "")
+ .contains(
+ "\"type\":\"object\",\"properties\":{\"bar\":{}}"));
+ }
+
+ @Test
+ public void openAIFunctionTest() {
+ KernelPlugin plugin = KernelPluginFactory.createFromObject(
+ new TestPlugin(),
+ "test");
+
+ Assertions.assertNotNull(plugin);
+ Assertions.assertEquals(plugin.getName(), "test");
+ Assertions.assertEquals(plugin.getFunctions().size(), 3);
+
+ KernelFunction> testFunction = plugin.getFunctions()
+ .get("asyncPersonFunction");
+ OpenAIFunction openAIFunction = OpenAIFunction.build(
+ testFunction.getMetadata(),
+ plugin.getName());
+
+ String parameters = "{\"type\":\"object\",\"required\":[\"person\",\"input\"],\"properties\":{\"input\":{\"type\":\"string\",\"description\":\"input string\"},\"person\":{\"type\":\"object\",\"properties\":{\"age\":{\"type\":\"integer\",\"description\":\"The age of the person.\"},\"name\":{\"type\":\"string\",\"description\":\"The name of the person.\"},\"title\":{\"type\":\"string\",\"enum\":[\"MS\",\"MRS\",\"MR\"],\"description\":\"The title of the person.\"}},\"required\":[\"age\",\"name\",\"title\"],\"additionalProperties\":false,\"description\":\"input person\"}}}";
+ Assertions.assertEquals(parameters,
+ openAIFunction.getFunctionDefinition().getParameters().toString());
+
+ }
+
+ public static class TestPlugin {
+
+ @DefineKernelFunction
+ public String testFunction(
+ @KernelFunctionParameter(name = "input", description = "input string") String input) {
+ return "test" + input;
+ }
+
+ @DefineKernelFunction(returnType = "int")
+ public Mono asyncTestFunction(
+ @KernelFunctionParameter(name = "input") String input) {
+ return Mono.just(1);
+ }
+
+ @DefineKernelFunction(returnType = "int", description = "test function description", name = "asyncPersonFunction", returnDescription = "test return description")
+ public Mono asyncPersonFunction(
+ @KernelFunctionParameter(name = "person", description = "input person", type = Person.class) Person person,
+ @KernelFunctionParameter(name = "input", description = "input string") String input) {
+ return Mono.just(1);
+ }
+ }
+
+ private static enum Title {
+ MS, MRS, MR
+ }
+
+ public static class Person {
+ @JsonPropertyDescription("The name of the person.")
+ private String name;
+ @JsonPropertyDescription("The age of the person.")
+ private int age;
+ @JsonPropertyDescription("The title of the person.")
+ private Title title;
+
+ public Person(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public Title getTitle() {
+ return title;
+ }
+
+ public void setTitle(Title title) {
+ this.title = title;
+ }
+ }
+
+}
diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiChatCompletionTest.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiChatCompletionTest.java
index 5a25e1aeb..5a1be82a4 100644
--- a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiChatCompletionTest.java
+++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAiChatCompletionTest.java
@@ -9,10 +9,11 @@
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.rest.Response;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.azure.json.JsonOptions;
+import com.azure.json.implementation.DefaultJsonReader;
import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader;
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
-import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
import com.microsoft.semantickernel.services.chatcompletion.AuthorRole;
import com.microsoft.semantickernel.services.chatcompletion.ChatHistory;
import java.nio.charset.Charset;
@@ -45,7 +46,7 @@ public void serializesToolCallsCorrectly() {
"a-tool-id",
"pluginName",
"funcName",
- KernelFunctionArguments.builder()
+ KernelArguments.builder()
.withVariable("id", "ca2fc6bc-1307-4da6-a009-d7bf88dec37b")
.build()))));
chatHistory.addMessage(new OpenAIChatMessageContent(
@@ -104,9 +105,9 @@ public ChatCompletions getValue() {
String message = EmbeddedResourceLoader.readFile("chatCompletion.txt",
OpenAiChatCompletionTest.class);
- return new ObjectMapper()
- .readValue(String.format(message, "Snuggles"),
- ChatCompletions.class);
+ return ChatCompletions.fromJson(
+ DefaultJsonReader.fromString(
+ String.format(message, "Snuggles"), new JsonOptions()));
} catch (Exception e) {
throw new RuntimeException(e);
}
diff --git a/api-test/integration-tests/pom.xml b/api-test/integration-tests/pom.xml
index b3ec65636..9b5c3883d 100644
--- a/api-test/integration-tests/pom.xml
+++ b/api-test/integration-tests/pom.xml
@@ -6,7 +6,7 @@
com.microsoft.semantic-kernel
api-test
- 1.2.2
+ 1.5.1-SNAPSHOT
../pom.xml
@@ -58,7 +58,27 @@
com.microsoft.semantic-kernel
- semantickernel-experimental
+ semantickernel-data-jdbc
+ test
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-data-mysql
+ test
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-data-hsqldb
+ test
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-data-sqlite
+ test
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-data-redis
test
@@ -70,13 +90,18 @@
com.mysql
mysql-connector-j
- 9.0.0
+ 9.6.0
test
org.postgresql
postgresql
- 42.7.3
+ 42.7.10
+
+
+ org.xerial
+ sqlite-jdbc
+ 3.53.0.0
@@ -97,7 +122,7 @@
com.redis
testcontainers-redis
- 2.2.2
+ 2.2.4
test
@@ -105,6 +130,31 @@
wiremock
test
+
+
+
+ com.github.victools
+ jsonschema-generator
+ test
+
+
+ com.github.victools
+ jsonschema-module-jackson
+ test
+
+
+
+
+ org.hsqldb
+ hsqldb
+ 2.7.4
+ test
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api-data
+ test
+
@@ -112,7 +162,7 @@
org.testcontainers
testcontainers-bom
- 1.18.3
+ 2.0.4
pom
import
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example03_ArgumentsTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example03_ArgumentsTest.java
index ac1821246..dd283952d 100644
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example03_ArgumentsTest.java
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example03_ArgumentsTest.java
@@ -6,7 +6,7 @@
import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
import com.microsoft.semantickernel.samples.syntaxexamples.functions.Example03_Arguments.StaticTextPlugin;
-import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -24,7 +24,7 @@ public void main() {
KernelPlugin functionCollection = KernelPluginFactory
.createFromObject(new StaticTextPlugin(), "text");
- KernelFunctionArguments arguments = KernelFunctionArguments.builder()
+ KernelArguments arguments = KernelArguments.builder()
.withInput("Today is: ")
.withVariable("day", "Monday")
.build();
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example05_InlineFunctionDefinitionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example05_InlineFunctionDefinitionTest.java
index f74268b25..d791ec9c2 100644
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example05_InlineFunctionDefinitionTest.java
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/Example05_InlineFunctionDefinitionTest.java
@@ -10,9 +10,9 @@
import com.microsoft.semantickernel.orchestration.FunctionResult;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
-import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt;
-import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
+
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@@ -68,7 +68,7 @@ public void main(WireMockRuntimeInfo wmRuntimeInfo) {
var result = kernel.invokeAsync(excuseFunction)
.withArguments(
- KernelFunctionArguments.builder()
+ KernelArguments.builder()
.withInput("I missed the F1 final race")
.build())
.block();
@@ -79,7 +79,7 @@ public void main(WireMockRuntimeInfo wmRuntimeInfo) {
result = kernel.invokeAsync(excuseFunction)
.withArguments(
- KernelFunctionArguments.builder()
+ KernelArguments.builder()
.withInput("sorry I forgot your birthday")
.build())
.block();
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ImportingMultiplePluginsTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ImportingMultiplePluginsTest.java
index dc7abe14a..e9e5c6d4a 100644
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ImportingMultiplePluginsTest.java
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ImportingMultiplePluginsTest.java
@@ -5,10 +5,11 @@
import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class ImportingMultiplePluginsTest {
-
+ @Disabled
@Test
public void canImportMultiplePlugins() {
KernelPlugin summarize = KernelPluginFactory.importPluginFromResourcesDirectory(
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/KernelHooksTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/KernelHooksTest.java
index 16b9cadaf..4a128fb16 100644
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/KernelHooksTest.java
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/KernelHooksTest.java
@@ -12,7 +12,7 @@
import com.microsoft.semantickernel.hooks.KernelHook.FunctionInvokingHook;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
-import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt;
import com.microsoft.semantickernel.semanticfunctions.OutputVariable;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
@@ -90,7 +90,7 @@ public void getUsageAsync(WireMockRuntimeInfo wmRuntimeInfo) {
kernel.invokeAsync(
excuseFunction)
.withArguments(
- KernelFunctionArguments
+ KernelArguments
.builder()
.withVariable("input", "I missed the F1 final race")
.build())
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java
index 39b11182a..ff4655d26 100644
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java
@@ -12,7 +12,7 @@
import com.microsoft.semantickernel.aiservices.openai.textcompletion.OpenAITextGenerationService;
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
-import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
+import com.microsoft.semantickernel.semanticfunctions.KernelArguments;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
@@ -49,7 +49,7 @@ public void textSemanticKernelTemplateXml() {
""")
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
.build())
- .withArguments(KernelFunctionArguments
+ .withArguments(KernelArguments
.builder()
.withVariable("value", "\"hello world\"")
.build())
@@ -71,7 +71,7 @@ public void textSemanticKernelTemplate() {
""")
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
.build())
- .withArguments(KernelFunctionArguments
+ .withArguments(KernelArguments
.builder()
.withVariable("value", "{{$ignore}}")
.withVariable("ignore", "dont show")
@@ -94,7 +94,7 @@ public void textHandleBarsTemplate() {
""")
.withTemplateFormat("handlebars")
.build())
- .withArguments(KernelFunctionArguments
+ .withArguments(KernelArguments
.builder()
.withVariable("value", "{{ignore}}")
.withVariable("ignore", "dont show")
@@ -117,7 +117,7 @@ public void chatSemanticKernelTemplateXml() {
""")
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
.build())
- .withArguments(KernelFunctionArguments
+ .withArguments(KernelArguments
.builder()
.withVariable("value", "\"hello world\"")
.build())
@@ -139,7 +139,7 @@ public void chatSemanticKernelTemplate() {
""")
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
.build())
- .withArguments(KernelFunctionArguments
+ .withArguments(KernelArguments
.builder()
.withVariable("value", "{{$ignore}}")
.withVariable("ignore", "dont show")
@@ -162,7 +162,7 @@ public void chatHandleBarsTemplate() {
""")
.withTemplateFormat("handlebars")
.build())
- .withArguments(KernelFunctionArguments
+ .withArguments(KernelArguments
.builder()
.withVariable("value", "{{ignore}}")
.withVariable("ignore", "dont show")
@@ -185,7 +185,7 @@ public void chatSemanticKernelTemplate2() {
""")
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
.build())
- .withArguments(KernelFunctionArguments
+ .withArguments(KernelArguments
.builder()
.withVariable("value", "{{$ignore}}")
.withVariable("ignore", "dont show")
@@ -240,10 +240,10 @@ public void canHandleIterableReturnFromFunction() {
.block();
String requestBody = wm.getAllServeEvents().get(0).getRequest().getBodyAsString();
- Assertions.assertTrue(requestBody.contains("{\"role\":\"user\",\"content\":\"[]"));
- Assertions.assertTrue(requestBody.contains("{\"role\":\"user\",\"content\":\"[1, 2, 3]"));
- Assertions.assertTrue(requestBody.contains("{\"role\":\"user\",\"content\":\"[4, 5, 6]"));
- Assertions.assertTrue(requestBody.contains("{\"role\":\"user\",\"content\":\"[7, 8, 9]"));
+ Assertions.assertTrue(requestBody.contains("\"content\":\"[]"));
+ Assertions.assertTrue(requestBody.contains("\"content\":\"[1, 2, 3]"));
+ Assertions.assertTrue(requestBody.contains("\"content\":\"[4, 5, 6]"));
+ Assertions.assertTrue(requestBody.contains("\"content\":\"[7, 8, 9]"));
}
private Kernel buildTextKernel() {
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ResponseSchemaTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ResponseSchemaTest.java
new file mode 100644
index 000000000..4ec677742
--- /dev/null
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ResponseSchemaTest.java
@@ -0,0 +1,311 @@
+package com.microsoft.semantickernel.tests;
+
+import com.azure.ai.openai.OpenAIAsyncClient;
+import com.azure.ai.openai.models.ChatCompletions;
+import com.azure.ai.openai.models.ChatCompletionsOptions;
+import com.azure.core.http.HttpHeaders;
+import com.azure.core.http.HttpRequest;
+import com.azure.core.http.rest.RequestOptions;
+import com.azure.core.http.rest.Response;
+import com.azure.json.JsonOptions;
+import com.azure.json.JsonWriter;
+import com.azure.json.implementation.DefaultJsonReader;
+import com.azure.json.implementation.DefaultJsonWriter;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.microsoft.semantickernel.Kernel;
+import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion;
+import com.microsoft.semantickernel.contextvariables.converters.ContextVariableJacksonConverter;
+import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader;
+import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader.ResourceLocation;
+import com.microsoft.semantickernel.orchestration.FunctionResult;
+import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
+import com.microsoft.semantickernel.orchestration.responseformat.JsonSchemaResponseFormat;
+import com.microsoft.semantickernel.semanticfunctions.HandlebarsPromptTemplateFactory;
+import com.microsoft.semantickernel.semanticfunctions.KernelFunctionYaml;
+import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
+import java.io.IOException;
+import java.io.StringWriter;
+import org.apache.commons.text.StringEscapeUtils;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import reactor.core.publisher.Mono;
+
+public class ResponseSchemaTest {
+
+ public static class TestClass {
+
+ private final String name;
+
+ @JsonCreator
+ public TestClass(
+ @JsonProperty("name") String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ @Test
+ public void sendsResponseSchemaFromTemplate() throws IOException {
+ OpenAIAsyncClient client = getOpenAIAsyncClient(
+ """
+ {
+ "name": "Test name"
+ }
+ """
+ .stripIndent());
+ Kernel kernel = buildKernel(client);
+
+ var getIntent = KernelFunctionYaml.fromPromptYaml(
+ EmbeddedResourceLoader
+ .readFile("responseSchema.prompt.yaml", ResponseSchemaTest.class,
+ ResourceLocation.CLASSPATH_ROOT),
+ new HandlebarsPromptTemplateFactory());
+
+ FunctionResult response = getIntent.invokeAsync(kernel)
+ .withResultTypeAutoConversion(TestClass.class)
+ .block();
+
+ verifyCalled(client,
+ """
+ {
+ "type":"json_schema",
+ "json_schema":{
+ "strict":true,
+ "name":"Test",
+ "schema":{
+ "type" : "object",
+ "properties" : {
+ "name" : {
+ "type" : "string"
+ }
+ },
+ "required" : [
+ "name"
+ ],
+ "additionalProperties" : false
+ }
+ }
+ }
+ """
+ );
+ }
+
+ @Disabled
+ @Test
+ public void sendsResponseSchema() {
+ OpenAIAsyncClient client = getOpenAIAsyncClient(
+ """
+ {
+ "name": "Test name"
+ }
+ """
+ .stripIndent());
+
+ Kernel kernel = buildKernel(client);
+
+ PromptExecutionSettings promptExecutionSettings = PromptExecutionSettings.builder()
+ .withResponseFormat(
+ JsonSchemaResponseFormat.builder()
+ .setResponseFormat(TestClass.class)
+ .setName("Test")
+ .build()
+ )
+ .build();
+
+ FunctionResult response = kernel.invokePromptAsync(
+ "Generate TestClass")
+ .withTypeConverter(ContextVariableJacksonConverter.create(TestClass.class))
+ .withResultType(TestClass.class)
+ .withPromptExecutionSettings(promptExecutionSettings)
+ .block();
+
+ verifyCalled(client,
+ """
+ {
+ "type":"json_schema",
+ "json_schema":{
+ "strict":true,
+ "name":"Test",
+ "schema":{
+ "type" : "object",
+ "properties" : {
+ "name" : {
+ "type" : "string"
+ }
+ },
+ "required" : [ "name" ],
+ "additionalProperties" : false
+ }
+ }
+ }
+ """
+ );
+ }
+
+ private static void verifyCalled(OpenAIAsyncClient client, String expected) {
+ Mockito.verify(client, Mockito.atLeastOnce())
+ .getChatCompletionsWithResponse(
+ Mockito.any(),
+ Mockito.argThat(
+ (ChatCompletionsOptions chatCompletionsOptions) -> {
+ StringWriter writer = new StringWriter();
+ try {
+ JsonWriter jsonWriter = DefaultJsonWriter.toWriter(
+ writer,
+ new JsonOptions()
+ );
+ JsonWriter format = chatCompletionsOptions.getResponseFormat()
+ .toJson(jsonWriter);
+ jsonWriter.flush();
+ writer.flush();
+
+ String json = String.valueOf(writer.getBuffer())
+ .replaceAll("\n", "")
+ .replaceAll("\r", "")
+ .replaceAll(" +", "");
+ String expectedClean = expected
+ .stripIndent()
+ .replaceAll("\n", "")
+ .replaceAll("\r", "")
+ .replaceAll(" +", "");
+
+ return json.equals(expectedClean);
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }),
+ Mockito.any());
+ }
+
+ private @NotNull OpenAIAsyncClient getOpenAIAsyncClient(String response) {
+ OpenAIAsyncClient client = Mockito.mock(OpenAIAsyncClient.class);
+
+ Mockito.when(
+ client.getChatCompletionsWithResponse(
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any()))
+ .thenReturn(
+ Mono.just(
+ new Response() {
+ @Override
+ public int getStatusCode() {
+ return 200;
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return new HttpHeaders();
+ }
+
+ @Override
+ public HttpRequest getRequest() {
+ return null;
+ }
+
+ @Override
+ public ChatCompletions getValue() {
+ return buildResponse(response);
+ }
+ }));
+ return client;
+ }
+
+
+ private ChatCompletions buildResponse(String response) {
+ String str = String.format("""
+ {
+ "choices" : [
+ {
+ "content_filter_results" : {
+ "hate" : {
+ "filtered" : false,
+ "severity" : "safe"
+ },
+ "self_harm" : {
+ "filtered" : false,
+ "severity" : "safe"
+ },
+ "sexual" : {
+ "filtered" : false,
+ "severity" : "safe"
+ },
+ "violence" : {
+ "filtered" : false,
+ "severity" : "safe"
+ }
+ },
+ "finish_reason" : "stop",
+ "index" : 0,
+ "message" : {
+ "content" : "%s",
+ "role" : "assistant"
+ }
+ }
+ ],
+ "created" : 1707253039,
+ "id" : "chatcmpl-xxx",
+ "prompt_filter_results" : [
+ {
+ "content_filter_results" : {
+ "hate" : {
+ "filtered" : false,
+ "severity" : "safe"
+ },
+ "self_harm" : {
+ "filtered" : false,
+ "severity" : "safe"
+ },
+ "sexual" : {
+ "filtered" : false,
+ "severity" : "safe"
+ },
+ "violence" : {
+ "filtered" : false,
+ "severity" : "safe"
+ }
+ },
+ "prompt_index" : 0
+ }
+ ],
+ "usage" : {
+ "completion_tokens" : 131,
+ "prompt_tokens" : 26,
+ "total_tokens" : 157
+ }
+ }
+ """, StringEscapeUtils.escapeJson(response));
+
+ try {
+ return ChatCompletions.fromJson(
+ DefaultJsonReader.fromString(
+ str, new JsonOptions())
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ private Kernel buildKernel(OpenAIAsyncClient client) {
+
+ ChatCompletionService openAIChatCompletion = OpenAIChatCompletion.builder()
+ .withOpenAIAsyncClient(client)
+ .withModelId("a-model")
+ .build();
+
+ Kernel kernel = Kernel.builder()
+ .withAIService(ChatCompletionService.class, openAIChatCompletion)
+ .build();
+
+ return kernel;
+
+ }
+}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java
index 852f55784..5f80a6e00 100644
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java
@@ -282,7 +282,7 @@ private ChatCompletionService getChatCompletionService() {
public static MappingBuilder buildTextResponse(String bodyMatcher, String responseBody) {
return post(urlEqualTo(
- "//openai/deployments/gpt-35-turbo-2/completions?api-version=2024-03-01-preview"))
+ "//openai/deployments/gpt-35-turbo-2/completions?api-version=2025-01-01-preview"))
.withRequestBody(new ContainsPattern(bodyMatcher))
.willReturn(
aResponse()
@@ -293,7 +293,7 @@ public static MappingBuilder buildTextResponse(String bodyMatcher, String respon
public static MappingBuilder buildResponse(String bodyMatcher, String responseBody) {
return post(urlEqualTo(
- "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2024-03-01-preview"))
+ "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2025-01-01-preview"))
.withRequestBody(new ContainsPattern(bodyMatcher))
.willReturn(
aResponse()
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/WiremockExamplesIT.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/WiremockExamplesIT.java
index 0eeabc135..f01b40d16 100644
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/WiremockExamplesIT.java
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/WiremockExamplesIT.java
@@ -32,8 +32,8 @@
import java.util.List;
import java.util.Queue;
import java.util.stream.Stream;
-import com.microsoft.semantickernel.samples.syntaxexamples.plugins.Example10_DescribeAllPluginsAndFunctions;
-import com.microsoft.semantickernel.samples.syntaxexamples.plugins.Example13_ConversationSummaryPlugin;
+import com.microsoft.semantickernel.samples.syntaxexamples.plugin.Example10_DescribeAllPluginsAndFunctions;
+import com.microsoft.semantickernel.samples.syntaxexamples.plugin.Example13_ConversationSummaryPlugin;
import com.microsoft.semantickernel.samples.syntaxexamples.template.Example06_TemplateLanguage;
import com.microsoft.semantickernel.samples.syntaxexamples.template.Example56_TemplateMethodFunctionsWithMultipleArguments;
import com.microsoft.semantickernel.samples.syntaxexamples.template.Example64_MultiplePromptTemplates;
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/Hotel.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/Hotel.java
deleted file mode 100644
index ad10ad645..000000000
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/Hotel.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.microsoft.semantickernel.tests.connectors.memory;
-
-import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordDataAttribute;
-import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordKeyAttribute;
-import com.microsoft.semantickernel.data.recordattributes.VectorStoreRecordVectorAttribute;
-
-import java.util.List;
-
-public class Hotel {
- @VectorStoreRecordKeyAttribute
- private final String id;
- @VectorStoreRecordDataAttribute
- private final String name;
- @VectorStoreRecordDataAttribute
- private final int code;
- @VectorStoreRecordDataAttribute(hasEmbedding = true, embeddingFieldName = "descriptionEmbedding")
- private final String description;
- @VectorStoreRecordVectorAttribute(dimensions = 3)
- private final List descriptionEmbedding;
- @VectorStoreRecordDataAttribute
- private double rating;
-
- public Hotel() {
- this(null, null, 0, null, null, 0.0);
- }
-
- public Hotel(String id, String name, int code, String description, List descriptionEmbedding, double rating) {
- this.id = id;
- this.name = name;
- this.code = code;
- this.description = description;
- this.descriptionEmbedding = descriptionEmbedding;
- this.rating = rating;
- }
-
- public String getId() {
- return id;
- }
-
- public String getName() {
- return name;
- }
-
- public int getCode() {
- return code;
- }
-
- public String getDescription() {
- return description;
- }
-
- public List getDescriptionEmbedding() {
- return descriptionEmbedding;
- }
-
- public double getRating() {
- return rating;
- }
-
- public void setRating(double rating) {
- this.rating = rating;
- }
-}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreRecordCollectionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreRecordCollectionTest.java
deleted file mode 100644
index 8bee5a762..000000000
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreRecordCollectionTest.java
+++ /dev/null
@@ -1,309 +0,0 @@
-package com.microsoft.semantickernel.tests.connectors.memory.jdbc;
-
-import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreQueryProvider;
-import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreRecordCollection;
-import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreRecordCollectionOptions;
-import com.microsoft.semantickernel.connectors.data.mysql.MySQLVectorStoreQueryProvider;
-import com.microsoft.semantickernel.connectors.data.postgres.PostgreSQLVectorStoreQueryProvider;
-import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions;
-import com.microsoft.semantickernel.tests.connectors.memory.Hotel;
-import com.mysql.cj.jdbc.MysqlDataSource;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.EnumSource;
-import org.postgresql.ds.PGSimpleDataSource;
-import org.testcontainers.containers.MySQLContainer;
-import org.testcontainers.containers.PostgreSQLContainer;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import org.testcontainers.utility.DockerImageName;
-
-import javax.annotation.Nonnull;
-import javax.sql.DataSource;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-
-@Testcontainers
-public class JDBCVectorStoreRecordCollectionTest {
-
- @Container
- private static final MySQLContainer> MYSQL_CONTAINER = new MySQLContainer<>("mysql:5.7.34");
-
- private static final DockerImageName PGVECTOR = DockerImageName.parse("pgvector/pgvector:pg16").asCompatibleSubstituteFor("postgres");
- @Container
- private static final PostgreSQLContainer> POSTGRESQL_CONTAINER = new PostgreSQLContainer<>(PGVECTOR);
-
- public enum QueryProvider {
- MySQL,
- PostgreSQL
- }
-
- private JDBCVectorStoreRecordCollection buildRecordCollection(QueryProvider provider, @Nonnull String collectionName) {
- JDBCVectorStoreQueryProvider queryProvider;
- DataSource dataSource;
-
- switch (provider) {
- case MySQL:
- MysqlDataSource mysqlDataSource = new MysqlDataSource();
- mysqlDataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl());
- mysqlDataSource.setUser(MYSQL_CONTAINER.getUsername());
- mysqlDataSource.setPassword(MYSQL_CONTAINER.getPassword());
- dataSource = mysqlDataSource;
- queryProvider = MySQLVectorStoreQueryProvider.builder()
- .withDataSource(dataSource)
- .build();
- break;
- case PostgreSQL:
- PGSimpleDataSource pgSimpleDataSource = new PGSimpleDataSource();
- pgSimpleDataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl());
- pgSimpleDataSource.setUser(POSTGRESQL_CONTAINER.getUsername());
- pgSimpleDataSource.setPassword(POSTGRESQL_CONTAINER.getPassword());
- dataSource = pgSimpleDataSource;
- queryProvider = PostgreSQLVectorStoreQueryProvider.builder()
- .withDataSource(dataSource)
- .build();
- break;
- default:
- throw new IllegalArgumentException("Unknown query provider: " + provider);
- }
-
-
- JDBCVectorStoreRecordCollection recordCollection = new JDBCVectorStoreRecordCollection<>(
- dataSource,
- collectionName,
- JDBCVectorStoreRecordCollectionOptions.builder()
- .withRecordClass(Hotel.class)
- .withQueryProvider(queryProvider)
- .build());
-
- recordCollection.prepareAsync().block();
- recordCollection.createCollectionIfNotExistsAsync().block();
- return recordCollection;
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void buildRecordCollection(QueryProvider provider) {
- assertNotNull(buildRecordCollection(provider, "buildTest"));
- }
-
- private List getHotels() {
- return List.of(
- new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", Arrays.asList(1.0f, 2.0f, 3.0f), 4.0),
- new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", Arrays.asList(1.0f, 2.0f, 3.0f), 3.0),
- new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", Arrays.asList(1.0f, 2.0f, 3.0f), 5.0),
- new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", Arrays.asList(1.0f, 2.0f, 3.0f), 4.0),
- new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", Arrays.asList(1.0f, 2.0f, 3.0f), 5.0)
- );
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void upsertAndGetRecordAsync(QueryProvider provider) {
- String collectionName = "upsertAndGetRecordAsync";
- JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, collectionName);
-
- List hotels = getHotels();
- for (Hotel hotel : hotels) {
- recordCollection.upsertAsync(hotel, null).block();
- }
-
- // Upsert the first time
- for (Hotel hotel : hotels) {
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
- assertNotNull(retrievedHotel);
- assertEquals(hotel.getId(), retrievedHotel.getId());
- assertEquals(hotel.getRating(), retrievedHotel.getRating());
-
- // Update the rating
- hotel.setRating(1.0);
- }
-
- // Upsert the second time with updated rating
- for (Hotel hotel : hotels) {
- recordCollection.upsertAsync(hotel, null).block();
- }
-
- for (Hotel hotel : hotels) {
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
- assertNotNull(retrievedHotel);
- assertEquals(hotel.getId(), retrievedHotel.getId());
- assertEquals(1.0, retrievedHotel.getRating());
- }
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void getBatchAsync(QueryProvider provider) {
- String collectionName = "getBatchAsync";
- JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, collectionName);
-
- List hotels = getHotels();
- for (Hotel hotel : hotels) {
- recordCollection.upsertAsync(hotel, null).block();
- }
-
- List keys = new ArrayList<>();
- for (Hotel hotel : hotels) {
- keys.add(hotel.getId());
- }
-
- List retrievedHotels = recordCollection.getBatchAsync(keys, null).block();
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void upsertBatchAndGetBatchAsync(QueryProvider provider) {
- String collectionName = "upsertBatchAndGetBatchAsync";
- JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, collectionName);
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- List keys = new ArrayList<>();
- for (Hotel hotel : hotels) {
- keys.add(hotel.getId());
- }
-
- List retrievedHotels = recordCollection.getBatchAsync(keys, null).block();
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void insertAndReplaceAsync(QueryProvider provider) {
- String collectionName = "insertAndReplaceAsync";
- JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, collectionName);
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
- recordCollection.upsertBatchAsync(hotels, null).block();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- List keys = new ArrayList<>();
- for (Hotel hotel : hotels) {
- keys.add(hotel.getId());
- }
-
- List retrievedHotels = recordCollection.getBatchAsync(keys, null).block();
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void deleteRecordAsync(QueryProvider provider) {
- String collectionName = "deleteRecordAsync";
- JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, collectionName);
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- for (Hotel hotel : hotels) {
- recordCollection.deleteAsync(hotel.getId(), null).block();
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
- assertNull(retrievedHotel);
- }
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void deleteBatchAsync(QueryProvider provider) {
- String collectionName = "deleteBatchAsync";
- JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, collectionName);
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- List keys = new ArrayList<>();
- for (Hotel hotel : hotels) {
- keys.add(hotel.getId());
- }
-
- recordCollection.deleteBatchAsync(keys, null).block();
-
- for (String key : keys) {
- Hotel retrievedHotel = recordCollection.getAsync(key, null).block();
- assertNull(retrievedHotel);
- }
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void getWithNoVectors(QueryProvider provider) {
- String collectionName = "getWithNoVectors";
- JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, collectionName);
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- GetRecordOptions options = GetRecordOptions.builder()
- .includeVectors(false)
- .build();
-
- for (Hotel hotel : hotels) {
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), options).block();
- assertNotNull(retrievedHotel);
- assertEquals(hotel.getId(), retrievedHotel.getId());
- assertNull(retrievedHotel.getDescriptionEmbedding());
- }
-
- options = GetRecordOptions.builder()
- .includeVectors(true)
- .build();
-
- for (Hotel hotel : hotels) {
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), options).block();
- assertNotNull(retrievedHotel);
- assertEquals(hotel.getId(), retrievedHotel.getId());
- assertNotNull(retrievedHotel.getDescriptionEmbedding());
- }
- }
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void getBatchWithNoVectors(QueryProvider provider) {
- String collectionName = "getBatchWithNoVectors";
- JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider, collectionName);
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- GetRecordOptions options = GetRecordOptions.builder()
- .includeVectors(false)
- .build();
-
- List keys = new ArrayList<>();
- for (Hotel hotel : hotels) {
- keys.add(hotel.getId());
- }
-
- List retrievedHotels = recordCollection.getBatchAsync(keys, options).block();
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
-
- for (Hotel hotel : retrievedHotels) {
- assertNull(hotel.getDescriptionEmbedding());
- }
-
- options = GetRecordOptions.builder()
- .includeVectors(true)
- .build();
-
- retrievedHotels = recordCollection.getBatchAsync(keys, options).block();
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
-
- for (Hotel hotel : retrievedHotels) {
- assertNotNull(hotel.getDescriptionEmbedding());
- }
- }
-}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreTest.java
deleted file mode 100644
index 8c2fbfd05..000000000
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/jdbc/JDBCVectorStoreTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.microsoft.semantickernel.tests.connectors.memory.jdbc;
-
-import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStore;
-import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreOptions;
-import com.microsoft.semantickernel.connectors.data.jdbc.JDBCVectorStoreQueryProvider;
-import com.microsoft.semantickernel.connectors.data.mysql.MySQLVectorStoreQueryProvider;
-import com.microsoft.semantickernel.connectors.data.postgres.PostgreSQLVectorStoreQueryProvider;
-import com.microsoft.semantickernel.tests.connectors.memory.Hotel;
-import com.mysql.cj.jdbc.MysqlDataSource;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.EnumSource;
-import org.postgresql.ds.PGSimpleDataSource;
-import org.testcontainers.containers.MySQLContainer;
-import org.testcontainers.containers.PostgreSQLContainer;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import org.testcontainers.utility.DockerImageName;
-
-import javax.annotation.Nonnull;
-import javax.sql.DataSource;
-import java.util.Arrays;
-import java.util.List;
-
-import com.microsoft.semantickernel.tests.connectors.memory.jdbc.JDBCVectorStoreRecordCollectionTest.QueryProvider;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@Testcontainers
-public class JDBCVectorStoreTest {
- @Container
- private static final MySQLContainer> MYSQL_CONTAINER = new MySQLContainer<>("mysql:5.7.34");
-
- private static final DockerImageName PGVECTOR = DockerImageName.parse("pgvector/pgvector:pg16").asCompatibleSubstituteFor("postgres");
- @Container
- private static final PostgreSQLContainer> POSTGRESQL_CONTAINER = new PostgreSQLContainer<>(PGVECTOR);
-
- private JDBCVectorStore buildVectorStore(QueryProvider provider) {
- JDBCVectorStoreQueryProvider queryProvider;
- DataSource dataSource;
-
- switch (provider) {
- case MySQL:
- MysqlDataSource mysqlDataSource = new MysqlDataSource();
- mysqlDataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl());
- mysqlDataSource.setUser(MYSQL_CONTAINER.getUsername());
- mysqlDataSource.setPassword(MYSQL_CONTAINER.getPassword());
- dataSource = mysqlDataSource;
- queryProvider = MySQLVectorStoreQueryProvider.builder()
- .withDataSource(dataSource)
- .build();
- break;
- case PostgreSQL:
- PGSimpleDataSource pgSimpleDataSource = new PGSimpleDataSource();
- pgSimpleDataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl());
- pgSimpleDataSource.setUser(POSTGRESQL_CONTAINER.getUsername());
- pgSimpleDataSource.setPassword(POSTGRESQL_CONTAINER.getPassword());
- dataSource = pgSimpleDataSource;
- queryProvider = PostgreSQLVectorStoreQueryProvider.builder()
- .withDataSource(dataSource)
- .build();
- break;
- default:
- throw new IllegalArgumentException("Unknown query provider: " + provider);
- }
-
-
- JDBCVectorStore vectorStore = JDBCVectorStore.builder()
- .withDataSource(dataSource)
- .withOptions(
- JDBCVectorStoreOptions.builder()
- .withQueryProvider(queryProvider)
- .build()
- )
- .build();
-
- vectorStore.prepareAsync().block();
- return vectorStore;
- }
-
-
- @ParameterizedTest
- @EnumSource(QueryProvider.class)
- public void getCollectionNamesAsync(QueryProvider provider) {
- JDBCVectorStore vectorStore = buildVectorStore(provider);
-
- vectorStore.getCollectionNamesAsync().block();
-
- List collectionNames = Arrays.asList("collection1", "collection2", "collection3");
-
- for (String collectionName : collectionNames) {
- vectorStore.getCollection(collectionName, Hotel.class, null).createCollectionAsync().block();
- }
-
- List retrievedCollectionNames = vectorStore.getCollectionNamesAsync().block();
- assertNotNull(retrievedCollectionNames);
- assertEquals(collectionNames.size(), retrievedCollectionNames.size());
- for (String collectionName : collectionNames) {
- assertTrue(retrievedCollectionNames.contains(collectionName));
- }
- }
-}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreRecordCollectionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreRecordCollectionTest.java
deleted file mode 100644
index db3b6d6cd..000000000
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreRecordCollectionTest.java
+++ /dev/null
@@ -1,305 +0,0 @@
-package com.microsoft.semantickernel.tests.connectors.memory.redis;
-
-import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStoreRecordCollection;
-import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStoreRecordCollectionOptions;
-import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDataField;
-import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition;
-import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordField;
-import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordKeyField;
-import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordVectorField;
-import com.microsoft.semantickernel.data.recordoptions.GetRecordOptions;
-import com.microsoft.semantickernel.tests.connectors.memory.Hotel;
-import com.redis.testcontainers.RedisContainer;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.MethodOrderer;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestMethodOrder;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.EnumSource;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import redis.clients.jedis.JedisPooled;
-
-import javax.annotation.Nonnull;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-@Testcontainers
-@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
-public class RedisVectorStoreRecordCollectionTest {
-
- @Container private static final RedisContainer redisContainer = new RedisContainer("redis/redis-stack:latest");
-
- private static final Map> optionsMap = new HashMap<>();
-
- public enum RecordCollectionOptions {
- DEFAULT, WITH_CUSTOM_DEFINITION
- }
-
- @BeforeAll
- static void setup() {
- optionsMap.put(RecordCollectionOptions.DEFAULT, RedisVectorStoreRecordCollectionOptions.builder()
- .withRecordClass(Hotel.class)
- .build());
-
- List fields = new ArrayList<>();
- fields.add(VectorStoreRecordKeyField.builder()
- .withName("id")
- .build());
- fields.add(VectorStoreRecordDataField.builder()
- .withName("name")
- .withFieldType(String.class)
- .build());
- fields.add(VectorStoreRecordDataField.builder()
- .withName("code")
- .withFieldType(Integer.class)
- .build());
- fields.add(VectorStoreRecordDataField.builder()
- .withName("description")
- .withFieldType(String.class)
- .withHasEmbedding(true)
- .withEmbeddingFieldName("descriptionEmbedding")
- .build());
- fields.add(VectorStoreRecordVectorField.builder()
- .withName("descriptionEmbedding")
- .withDimensions(768)
- .build());
- fields.add(VectorStoreRecordDataField.builder()
- .withName("rating")
- .withFieldType(Double.class)
- .build());
- VectorStoreRecordDefinition recordDefinition = VectorStoreRecordDefinition.fromFields(fields);
-
- optionsMap.put(RecordCollectionOptions.WITH_CUSTOM_DEFINITION, RedisVectorStoreRecordCollectionOptions.builder()
- .withRecordClass(Hotel.class)
- .withRecordDefinition(recordDefinition)
- .build());
- }
-
- private RedisVectorStoreRecordCollection buildrecordCollection(@Nonnull RedisVectorStoreRecordCollectionOptions options, @Nonnull String collectionName) {
- return new RedisVectorStoreRecordCollection<>(new JedisPooled(redisContainer.getRedisURI()), collectionName, RedisVectorStoreRecordCollectionOptions.builder()
- .withRecordClass(options.getRecordClass())
- .withVectorStoreRecordMapper(options.getVectorStoreRecordMapper())
- .withRecordDefinition(options.getRecordDefinition())
- .withPrefixCollectionName(options.isPrefixCollectionName())
- .build());
- }
-
- private List getHotels() {
- return List.of(
- new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description", Arrays.asList(1.0f, 2.0f, 3.0f), 4.0),
- new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description", Arrays.asList(1.0f, 2.0f, 3.0f), 3.0),
- new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description", Arrays.asList(1.0f, 2.0f, 3.0f), 5.0),
- new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description", Arrays.asList(1.0f, 2.0f, 3.0f), 4.0),
- new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description", Arrays.asList(1.0f, 2.0f, 3.0f), 5.0)
- );
- }
-
- @Order(1)
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void buildrecordCollection(RecordCollectionOptions options) {
- assertNotNull(buildrecordCollection(optionsMap.get(options), options.name()));
- }
-
- @Order(2)
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void createCollectionAsync(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- assertEquals(false, recordCollection.collectionExistsAsync().block());
- recordCollection.createCollectionAsync().block();
- assertEquals(true, recordCollection.collectionExistsAsync().block());
- }
-
- @Test
- public void deleteCollectionAsync() {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(RecordCollectionOptions.DEFAULT), "deleteCollectionAsync");
-
- assertEquals(false, recordCollection.collectionExistsAsync().block());
- recordCollection.createCollectionAsync().block();
- recordCollection.deleteCollectionAsync().block();
- assertEquals(false, recordCollection.collectionExistsAsync().block());
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void upsertAndGetRecordAsync(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- for (Hotel hotel : hotels) {
- recordCollection.upsertAsync(hotel, null).block();
- }
-
- for (Hotel hotel : hotels) {
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
- assertNotNull(retrievedHotel);
- assertEquals(hotel.getId(), retrievedHotel.getId());
- }
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void getBatchAsync(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- for (Hotel hotel : hotels) {
- recordCollection.upsertAsync(hotel, null).block();
- }
-
- List ids = new ArrayList<>();
- hotels.forEach(hotel -> ids.add(hotel.getId()));
-
- List retrievedHotels = recordCollection.getBatchAsync(ids, null).block();
-
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
- for (int i = 0; i < hotels.size(); i++) {
- assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId());
- }
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void upsertBatchAsync(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- List keys = recordCollection.upsertBatchAsync(hotels, null).block();
- assertNotNull(keys);
-
- List retrievedHotels = (List) recordCollection.getBatchAsync(keys, null).block();
-
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
- for (int i = 0; i < hotels.size(); i++) {
- assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId());
- }
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void deleteAsync(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- for (Hotel hotel : hotels) {
- recordCollection.deleteAsync(hotel.getId(), null).block();
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
- assertNull(retrievedHotel);
- }
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void deleteBatchAsync(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- List ids = new ArrayList<>();
- hotels.forEach(hotel -> ids.add(hotel.getId()));
-
- recordCollection.deleteBatchAsync(ids, null).block();
-
- for (String id : ids) {
- Hotel retrievedHotel = recordCollection.getAsync(id, null).block();
- assertNull(retrievedHotel);
- }
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void getAsyncWithVectors(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- for (Hotel hotel : hotels) {
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
- assertNotNull(retrievedHotel);
- assertNotNull(retrievedHotel.getDescriptionEmbedding());
- assertEquals(hotel.getId(), retrievedHotel.getId());
- assertEquals(hotel.getDescription(), retrievedHotel.getDescription());
- }
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void getBatchAsyncWithVectors(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- List ids = new ArrayList<>();
- hotels.forEach(hotel -> ids.add(hotel.getId()));
-
- List retrievedHotels = recordCollection.getBatchAsync(ids, null).block();
-
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
- for (int i = 0; i < hotels.size(); i++) {
- assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId());
- assertEquals(hotels.get(i).getDescription(), retrievedHotels.get(i).getDescription());
- assertNotNull(retrievedHotels.get(i).getDescriptionEmbedding());
- }
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void getAsyncWithNoVectors(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- GetRecordOptions getRecordOptions = GetRecordOptions.builder().includeVectors(false).build();
- for (Hotel hotel : hotels) {
- Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), getRecordOptions).block();
- assertNotNull(retrievedHotel);
- assertNull(retrievedHotel.getDescriptionEmbedding());
- assertEquals(hotel.getId(), retrievedHotel.getId());
- assertEquals(hotel.getDescription(), retrievedHotel.getDescription());
- }
- }
-
- @ParameterizedTest
- @EnumSource(RecordCollectionOptions.class)
- public void getBatchAsyncWithNoVectors(RecordCollectionOptions options) {
- RedisVectorStoreRecordCollection recordCollection = buildrecordCollection(optionsMap.get(options), options.name());
-
- List hotels = getHotels();
- recordCollection.upsertBatchAsync(hotels, null).block();
-
- GetRecordOptions getRecordOptions = GetRecordOptions.builder().includeVectors(false).build();
- List ids = new ArrayList<>();
- hotels.forEach(hotel -> ids.add(hotel.getId()));
-
- List retrievedHotels = recordCollection.getBatchAsync(ids, getRecordOptions).block();
-
- assertNotNull(retrievedHotels);
- assertEquals(hotels.size(), retrievedHotels.size());
- for (int i = 0; i < hotels.size(); i++) {
- assertEquals(hotels.get(i).getId(), retrievedHotels.get(i).getId());
- assertEquals(hotels.get(i).getDescription(), retrievedHotels.get(i).getDescription());
- assertNull(retrievedHotels.get(i).getDescriptionEmbedding());
- }
- }
-}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreTest.java
deleted file mode 100644
index 96341053e..000000000
--- a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/connectors/memory/redis/RedisVectorStoreTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.microsoft.semantickernel.tests.connectors.memory.redis;
-
-import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStore;
-import com.microsoft.semantickernel.connectors.data.redis.RedisVectorStoreOptions;
-import com.microsoft.semantickernel.data.recorddefinition.VectorStoreRecordDefinition;
-import com.microsoft.semantickernel.tests.connectors.memory.Hotel;
-import com.redis.testcontainers.RedisContainer;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import redis.clients.jedis.JedisPooled;
-
-import java.util.Arrays;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@Testcontainers
-public class RedisVectorStoreTest {
- @Container
- private static final RedisContainer redisContainer = new RedisContainer("redis/redis-stack:latest");
- private static JedisPooled jedis;
-
- @BeforeAll
- public static void setUp() {
- jedis = new JedisPooled(redisContainer.getRedisURI());
- }
-
- @Test
- public void getCollectionNamesAsync() {
- RedisVectorStore vectorStore = new RedisVectorStore(jedis, new RedisVectorStoreOptions());
- List collectionNames = Arrays.asList("collection1", "collection2", "collection3");
-
- for (String collectionName : collectionNames) {
- vectorStore.getCollection(collectionName, Hotel.class, null).createCollectionAsync().block();
- }
-
- List retrievedCollectionNames = vectorStore.getCollectionNamesAsync().block();
- assertNotNull(retrievedCollectionNames);
- assertEquals(collectionNames.size(), retrievedCollectionNames.size());
- for (String collectionName : collectionNames) {
- assertTrue(retrievedCollectionNames.contains(collectionName));
- }
- }
-}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/Hotel.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/Hotel.java
new file mode 100644
index 000000000..6b33c380b
--- /dev/null
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/Hotel.java
@@ -0,0 +1,113 @@
+package com.microsoft.semantickernel.tests.data.jdbc;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData;
+import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey;
+import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector;
+import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction;
+import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind;
+
+import java.util.List;
+
+public class Hotel {
+ @JsonProperty("hotelId")
+ @VectorStoreRecordKey
+ private final String id;
+
+ @VectorStoreRecordData(isFilterable = true)
+ private final String name;
+
+ @VectorStoreRecordData
+ private final int code;
+
+ @JsonProperty("summary")
+ @VectorStoreRecordData()
+ private final String description;
+
+ @JsonProperty("summaryEmbedding1")
+ @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.EUCLIDEAN_DISTANCE)
+ private final List euclidean;
+
+ @JsonProperty("summaryEmbedding2")
+ @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.COSINE_DISTANCE)
+ private final List cosineDistance;
+
+ @JsonProperty("summaryEmbedding3")
+ @VectorStoreRecordVector(dimensions = 8, distanceFunction = DistanceFunction.DOT_PRODUCT)
+ private final List dotProduct;
+
+ @JsonProperty("indexedSummaryEmbedding")
+ @VectorStoreRecordVector(dimensions = 8, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.EUCLIDEAN_DISTANCE)
+ private final List indexedEuclidean;
+
+ @VectorStoreRecordData(isFilterable = true)
+ private final List tags;
+
+ @VectorStoreRecordData
+ private double rating;
+
+ public Hotel() {
+ this(null, null, 0, null, null, null, null, null, 0.0, null);
+ }
+
+ @JsonCreator
+ public Hotel(
+ @JsonProperty("hotelId") String id,
+ @JsonProperty("name") String name,
+ @JsonProperty("code") int code,
+ @JsonProperty("summary") String description,
+ @JsonProperty("summaryEmbedding1") List euclidean,
+ @JsonProperty("summaryEmbedding2") List cosineDistance,
+ @JsonProperty("summaryEmbedding3") List dotProduct,
+ @JsonProperty("indexedSummaryEmbedding") List indexedEuclidean,
+ @JsonProperty("rating") double rating,
+ @JsonProperty("tags") List tags) {
+ this.id = id;
+ this.name = name;
+ this.code = code;
+ this.description = description;
+ this.euclidean = euclidean;
+ this.cosineDistance = euclidean;
+ this.dotProduct = euclidean;
+ this.indexedEuclidean = euclidean;
+ this.rating = rating;
+ this.tags = tags;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public List getEuclidean() {
+ return euclidean;
+ }
+
+ public List getIndexedEuclidean() {
+ return indexedEuclidean;
+ }
+
+ public double getRating() {
+ return rating;
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public void setRating(double rating) {
+ this.rating = rating;
+ }
+}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreRecordCollectionTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreRecordCollectionTest.java
new file mode 100644
index 000000000..6bc99f25d
--- /dev/null
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreRecordCollectionTest.java
@@ -0,0 +1,551 @@
+package com.microsoft.semantickernel.tests.data.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.microsoft.semantickernel.data.jdbc.hsqldb.HSQLDBVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollection;
+import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions;
+import com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.jdbc.mysql.MySQLVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.jdbc.sqlite.SQLiteVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.vectorsearch.VectorSearchFilter;
+import com.microsoft.semantickernel.data.vectorsearch.VectorSearchResult;
+import com.microsoft.semantickernel.data.vectorstorage.options.GetRecordOptions;
+import com.microsoft.semantickernel.data.vectorstorage.options.VectorSearchOptions;
+import com.mysql.cj.jdbc.MysqlDataSource;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Stream;
+import javax.annotation.Nonnull;
+import javax.sql.DataSource;
+import org.hsqldb.jdbc.JDBCDataSourceFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.postgresql.ds.PGSimpleDataSource;
+import org.sqlite.SQLiteDataSource;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+
+@Testcontainers
+public class JDBCVectorStoreRecordCollectionTest {
+
+ @Container
+ private static final MySQLContainer> MYSQL_CONTAINER = new MySQLContainer<>("mysql:5.7.34");
+
+ private static final DockerImageName PGVECTOR = DockerImageName.parse("pgvector/pgvector:pg16")
+ .asCompatibleSubstituteFor("postgres");
+ @Container
+ private static final PostgreSQLContainer> POSTGRESQL_CONTAINER = new PostgreSQLContainer<>(
+ PGVECTOR);
+
+ public enum QueryProvider {
+ MySQL,
+ PostgreSQL,
+ SQLite,
+ HSQLDB
+ }
+
+ static Path createTempDbFile(String prefix) {
+ try {
+ Path file = Files.createTempFile(prefix, ".db");
+ file.toFile().deleteOnExit();
+ return file;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private JDBCVectorStoreRecordCollection buildRecordCollection(QueryProvider provider,
+ @Nonnull String collectionName) {
+ SQLVectorStoreQueryProvider queryProvider;
+ DataSource dataSource;
+
+ switch (provider) {
+ case MySQL:
+ MysqlDataSource mysqlDataSource = new MysqlDataSource();
+ mysqlDataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl());
+ mysqlDataSource.setUser(MYSQL_CONTAINER.getUsername());
+ mysqlDataSource.setPassword(MYSQL_CONTAINER.getPassword());
+ dataSource = mysqlDataSource;
+ queryProvider = MySQLVectorStoreQueryProvider.builder()
+ .withDataSource(dataSource)
+ .build();
+ break;
+ case PostgreSQL:
+ PGSimpleDataSource pgSimpleDataSource = new PGSimpleDataSource();
+ pgSimpleDataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl());
+ pgSimpleDataSource.setUser(POSTGRESQL_CONTAINER.getUsername());
+ pgSimpleDataSource.setPassword(POSTGRESQL_CONTAINER.getPassword());
+ dataSource = pgSimpleDataSource;
+ queryProvider = PostgreSQLVectorStoreQueryProvider.builder()
+ .withDataSource(dataSource)
+ .build();
+ break;
+ case SQLite:
+ Path sqliteDb = createTempDbFile("sqliteDb");
+ SQLiteDataSource sqliteDataSource = new SQLiteDataSource();
+ sqliteDataSource.setUrl("jdbc:sqlite:file:" + sqliteDb.toFile().getAbsolutePath());
+ dataSource = sqliteDataSource;
+
+ queryProvider = SQLiteVectorStoreQueryProvider.builder()
+ .withDataSource(dataSource)
+ .build();
+ break;
+ case HSQLDB:
+ try {
+ Path file = createTempDbFile("testHSQLDB");
+
+ Properties properties = new Properties();
+ properties.putAll(
+ Map.of(
+ "url", "jdbc:hsqldb:file:" + file.toFile().getAbsolutePath()
+ + ";sql.syntax_mys=true",
+ "user", "SA",
+ "password", ""
+ )
+ );
+
+ dataSource = JDBCDataSourceFactory.createDataSource(properties);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ queryProvider = HSQLDBVectorStoreQueryProvider.builder()
+ .withDataSource(dataSource)
+ .build();
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown query provider: " + provider);
+ }
+
+ JDBCVectorStoreRecordCollection recordCollection = new JDBCVectorStoreRecordCollection<>(
+ dataSource,
+ collectionName,
+ JDBCVectorStoreRecordCollectionOptions.builder()
+ .withRecordClass(Hotel.class)
+ .withQueryProvider(queryProvider)
+ .build());
+
+ recordCollection.prepareAsync().block();
+ recordCollection.createCollectionIfNotExistsAsync().block();
+ return recordCollection;
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void buildRecordCollection(QueryProvider provider) {
+ assertNotNull(buildRecordCollection(provider, "buildTest"));
+ }
+
+ private List getHotels() {
+ return Arrays.asList(
+ new Hotel("id_1", "Hotel 1", 1, "Hotel 1 description",
+ Arrays.asList(0.5f, 3.2f, 7.1f, -4.0f, 2.8f, 10.0f, -1.3f, 5.5f), null, null, null,
+ 4.0, Arrays.asList("luxury", "city")),
+ new Hotel("id_2", "Hotel 2", 2, "Hotel 2 description",
+ Arrays.asList(-2.0f, 8.1f, 0.9f, 5.4f, -3.3f, 2.2f, 9.9f, -4.5f), null, null, null,
+ 4.0, Arrays.asList("luxury", "city")),
+ new Hotel("id_3", "Hotel 3", 3, "Hotel 3 description",
+ Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f, -0.8f, 1.1f, -2.2f, 8.3f), null, null, null,
+ 5.0, Arrays.asList("luxury", "beach")),
+ new Hotel("id_4", "Hotel 4", 4, "Hotel 4 description",
+ Arrays.asList(7.0f, 1.2f, -5.3f, 2.5f, 6.6f, -7.8f, 3.9f, -0.1f), null, null, null,
+ 4.0, Arrays.asList("luxury", "city")),
+ new Hotel("id_5", "Hotel 5", 5, "Hotel 5 description",
+ Arrays.asList(-3.5f, 4.4f, -1.2f, 9.9f, 5.7f, -6.1f, 7.8f, -2.0f), null, null, null,
+ 4.0, Arrays.asList("luxury", "city"))
+ );
+ }
+
+ /**
+ * Search embeddings similar to the third hotel embeddings. In order of similarity: 1. Hotel 3
+ * 2. Hotel 1 3. Hotel 4
+ */
+ private static final List SEARCH_EMBEDDINGS = Arrays.asList(4.5f, -6.2f, 3.1f, 7.7f,
+ -0.8f, 1.1f, -2.2f, 8.2f);
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void upsertAndGetRecordAsync(QueryProvider provider) {
+ String collectionName = "upsertAndGetRecordAsync";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ for (Hotel hotel : hotels) {
+ recordCollection.upsertAsync(hotel, null).block();
+ }
+
+ // Upsert the first time
+ for (Hotel hotel : hotels) {
+ Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
+ assertNotNull(retrievedHotel);
+ assertEquals(hotel.getId(), retrievedHotel.getId());
+ assertEquals(hotel.getRating(), retrievedHotel.getRating());
+
+ // Update the rating
+ hotel.setRating(1.0);
+ }
+
+ // Upsert the second time with updated rating
+ for (Hotel hotel : hotels) {
+ recordCollection.upsertAsync(hotel, null).block();
+ }
+
+ for (Hotel hotel : hotels) {
+ Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
+ assertNotNull(retrievedHotel);
+ assertEquals(hotel.getId(), retrievedHotel.getId());
+ assertEquals(1.0, retrievedHotel.getRating());
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void getBatchAsync(QueryProvider provider) {
+ String collectionName = "getBatchAsync";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ for (Hotel hotel : hotels) {
+ recordCollection.upsertAsync(hotel, null).block();
+ }
+
+ List keys = new ArrayList<>();
+ for (Hotel hotel : hotels) {
+ keys.add(hotel.getId());
+ }
+
+ List retrievedHotels = recordCollection.getBatchAsync(keys, null).block();
+ assertNotNull(retrievedHotels);
+ assertEquals(hotels.size(), retrievedHotels.size());
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void upsertBatchAndGetBatchAsync(QueryProvider provider) {
+ String collectionName = "upsertBatchAndGetBatchAsync";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ List keys = new ArrayList<>();
+ for (Hotel hotel : hotels) {
+ keys.add(hotel.getId());
+ }
+
+ List retrievedHotels = recordCollection.getBatchAsync(keys, null).block();
+ assertNotNull(retrievedHotels);
+ assertEquals(hotels.size(), retrievedHotels.size());
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void insertAndReplaceAsync(QueryProvider provider) {
+ String collectionName = "insertAndReplaceAsync";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ List keys = new ArrayList<>();
+ for (Hotel hotel : hotels) {
+ keys.add(hotel.getId());
+ }
+
+ List retrievedHotels = recordCollection.getBatchAsync(keys, null).block();
+ assertNotNull(retrievedHotels);
+ assertEquals(hotels.size(), retrievedHotels.size());
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void deleteRecordAsync(QueryProvider provider) {
+ String collectionName = "deleteRecordAsync";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ for (Hotel hotel : hotels) {
+ recordCollection.deleteAsync(hotel.getId(), null).block();
+ Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), null).block();
+ assertNull(retrievedHotel);
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void deleteBatchAsync(QueryProvider provider) {
+ String collectionName = "deleteBatchAsync";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ List keys = new ArrayList<>();
+ for (Hotel hotel : hotels) {
+ keys.add(hotel.getId());
+ }
+
+ recordCollection.deleteBatchAsync(keys, null).block();
+
+ for (String key : keys) {
+ Hotel retrievedHotel = recordCollection.getAsync(key, null).block();
+ assertNull(retrievedHotel);
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void getWithNoVectors(QueryProvider provider) {
+ String collectionName = "getWithNoVectors";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ GetRecordOptions options = GetRecordOptions.builder()
+ .includeVectors(false)
+ .build();
+
+ for (Hotel hotel : hotels) {
+ Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), options).block();
+ assertNotNull(retrievedHotel);
+ assertEquals(hotel.getId(), retrievedHotel.getId());
+ assertNull(retrievedHotel.getEuclidean());
+ }
+
+ options = GetRecordOptions.builder()
+ .includeVectors(true)
+ .build();
+
+ for (Hotel hotel : hotels) {
+ Hotel retrievedHotel = recordCollection.getAsync(hotel.getId(), options).block();
+ assertNotNull(retrievedHotel);
+ assertEquals(hotel.getId(), retrievedHotel.getId());
+ assertNotNull(retrievedHotel.getEuclidean());
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void getBatchWithNoVectors(QueryProvider provider) {
+ String collectionName = "getBatchWithNoVectors";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ GetRecordOptions options = GetRecordOptions.builder()
+ .includeVectors(false)
+ .build();
+
+ List keys = new ArrayList<>();
+ for (Hotel hotel : hotels) {
+ keys.add(hotel.getId());
+ }
+
+ List retrievedHotels = recordCollection.getBatchAsync(keys, options).block();
+ assertNotNull(retrievedHotels);
+ assertEquals(hotels.size(), retrievedHotels.size());
+
+ for (Hotel hotel : retrievedHotels) {
+ assertNull(hotel.getEuclidean());
+ }
+
+ options = GetRecordOptions.builder()
+ .includeVectors(true)
+ .build();
+
+ retrievedHotels = recordCollection.getBatchAsync(keys, options).block();
+ assertNotNull(retrievedHotels);
+ assertEquals(hotels.size(), retrievedHotels.size());
+
+ for (Hotel hotel : retrievedHotels) {
+ assertNotNull(hotel.getEuclidean());
+ }
+ }
+
+ private static Stream provideSearchParameters() {
+ return Arrays.stream(QueryProvider.values()).map(provider ->
+ Stream.of(
+ Arguments.of(provider, "euclidean"),
+ Arguments.of(provider, "cosineDistance"),
+ Arguments.of(provider, "dotProduct")
+ )
+ ).flatMap(s -> s);
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideSearchParameters")
+ public void exactSearch(QueryProvider provider, String embeddingName) {
+ String collectionName = "search" + embeddingName;
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ VectorSearchOptions options = VectorSearchOptions.builder()
+ .withVectorFieldName(embeddingName)
+ .withTop(3)
+ .build();
+
+ // Embeddings similar to the third hotel
+ List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS,
+ options).block().getResults();
+ assertNotNull(results);
+ assertEquals(3, results.size());
+ // The third hotel should be the most similar
+ assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId());
+
+ options = VectorSearchOptions.builder()
+ .withVectorFieldName(embeddingName)
+ .withSkip(1)
+ .withTop(-100)
+ .build();
+
+ // Skip the first result
+ results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, options).block().getResults();
+ assertNotNull(results);
+ assertEquals(1, results.size());
+ // The first hotel should be the most similar
+ assertEquals(hotels.get(0).getId(), results.get(0).getRecord().getId());
+ }
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void approximateSearch(QueryProvider provider) {
+ String collectionName = "searchWithIndex";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ VectorSearchOptions options = VectorSearchOptions.builder()
+ .withVectorFieldName("indexedEuclidean")
+ .withTop(5)
+ .build();
+
+ // Embeddings similar to the third hotel
+ List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS,
+ options).block().getResults();
+ assertNotNull(results);
+ assertEquals(5, results.size());
+ // The third hotel should be the most similar
+ assertEquals("id_3", results.get(0).getRecord().getId());
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideSearchParameters")
+ public void searchWithFilterEqualToFilter(QueryProvider provider, String embeddingName) {
+ String collectionName = "searchWithFilterEqualToFilter";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ VectorSearchOptions options = VectorSearchOptions.builder()
+ .withVectorFieldName(embeddingName)
+ .withTop(3)
+ .withVectorSearchFilter(
+ VectorSearchFilter.builder()
+ .equalTo("rating", 4.0).build())
+ .build();
+
+ // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned
+ List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS,
+ options).block().getResults();
+ assertNotNull(results);
+ assertEquals(3, results.size());
+ // The first hotel should be the most similar
+ assertEquals("id_1", results.get(0).getRecord().getId());
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideSearchParameters")
+ public void searchWithAnyTagEqualToFilter(QueryProvider provider, String embeddingName) {
+ String collectionName = "searchWithAnyTagEqualToFilter";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(provider,
+ collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ VectorSearchOptions options = VectorSearchOptions.builder()
+ .withVectorFieldName(embeddingName)
+ .withTop(3)
+ .withVectorSearchFilter(
+ VectorSearchFilter.builder()
+ .anyTagEqualTo("tags", "city").build())
+ .build();
+
+ // Embeddings similar to the third hotel, but as the filter is set to 4.0, the third hotel should not be returned
+ List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS,
+ options).block().getResults();
+ assertNotNull(results);
+ assertEquals(3, results.size());
+ // The first hotel should be the most similar
+ assertEquals("id_1", results.get(0).getRecord().getId());
+ }
+
+ // MySQL will always return the vectors as they're needed to compute the distances
+ @Test
+ public void postgresSearchIncludeAndNotIncludeVectors() {
+ String collectionName = "searchIncludeAndNotIncludeVectors";
+ JDBCVectorStoreRecordCollection recordCollection = buildRecordCollection(
+ QueryProvider.PostgreSQL, collectionName);
+
+ List hotels = getHotels();
+ recordCollection.upsertBatchAsync(hotels, null).block();
+
+ List> results = recordCollection.searchAsync(SEARCH_EMBEDDINGS,
+ null).block().getResults();
+ assertNotNull(results);
+ assertEquals(3, results.size());
+ // The third hotel should be the most similar
+ assertEquals(hotels.get(2).getId(), results.get(0).getRecord().getId());
+ assertNull(results.get(0).getRecord().getEuclidean());
+
+ VectorSearchOptions options = VectorSearchOptions.builder()
+ .withIncludeVectors(true)
+ .build();
+
+ results = recordCollection.searchAsync(SEARCH_EMBEDDINGS, options).block().getResults();
+ assertNotNull(results);
+ assertEquals(3, results.size());
+ // The third hotel should be the most similar
+ assertEquals("id_3", results.get(0).getRecord().getId());
+ assertNotNull(results.get(0).getRecord().getEuclidean());
+ }
+}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreTest.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreTest.java
new file mode 100644
index 000000000..ce62c70d8
--- /dev/null
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/jdbc/JDBCVectorStoreTest.java
@@ -0,0 +1,148 @@
+package com.microsoft.semantickernel.tests.data.jdbc;
+
+import com.microsoft.semantickernel.data.jdbc.hsqldb.HSQLDBVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.jdbc.JDBCVectorStore;
+import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreOptions;
+import com.microsoft.semantickernel.data.jdbc.SQLVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.jdbc.JDBCVectorStoreRecordCollectionOptions;
+import com.microsoft.semantickernel.data.jdbc.mysql.MySQLVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.jdbc.postgres.PostgreSQLVectorStoreQueryProvider;
+import com.microsoft.semantickernel.data.jdbc.sqlite.SQLiteVectorStoreQueryProvider;
+import com.mysql.cj.jdbc.MysqlDataSource;
+import org.hsqldb.jdbc.JDBCDataSourceFactory;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.postgresql.ds.PGSimpleDataSource;
+import org.sqlite.SQLiteDataSource;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import javax.sql.DataSource;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import com.microsoft.semantickernel.tests.data.jdbc.JDBCVectorStoreRecordCollectionTest.QueryProvider;
+
+import static com.microsoft.semantickernel.tests.data.jdbc.JDBCVectorStoreRecordCollectionTest.createTempDbFile;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+public class JDBCVectorStoreTest {
+
+ @Container
+ private static final MySQLContainer> MYSQL_CONTAINER = new MySQLContainer<>("mysql:5.7.34");
+
+ private static final DockerImageName PGVECTOR = DockerImageName.parse("pgvector/pgvector:pg16")
+ .asCompatibleSubstituteFor("postgres");
+ @Container
+ private static final PostgreSQLContainer> POSTGRESQL_CONTAINER = new PostgreSQLContainer<>(
+ PGVECTOR);
+
+ private JDBCVectorStore buildVectorStore(QueryProvider provider) {
+ SQLVectorStoreQueryProvider queryProvider;
+ DataSource dataSource;
+
+ switch (provider) {
+ case MySQL:
+ MysqlDataSource mysqlDataSource = new MysqlDataSource();
+ mysqlDataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl());
+ mysqlDataSource.setUser(MYSQL_CONTAINER.getUsername());
+ mysqlDataSource.setPassword(MYSQL_CONTAINER.getPassword());
+ dataSource = mysqlDataSource;
+ queryProvider = MySQLVectorStoreQueryProvider.builder()
+ .withDataSource(dataSource)
+ .build();
+ break;
+ case PostgreSQL:
+ PGSimpleDataSource pgSimpleDataSource = new PGSimpleDataSource();
+ pgSimpleDataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl());
+ pgSimpleDataSource.setUser(POSTGRESQL_CONTAINER.getUsername());
+ pgSimpleDataSource.setPassword(POSTGRESQL_CONTAINER.getPassword());
+ dataSource = pgSimpleDataSource;
+ queryProvider = PostgreSQLVectorStoreQueryProvider.builder()
+ .withDataSource(dataSource)
+ .build();
+ break;
+ case SQLite:
+ Path sqliteDb = createTempDbFile("testSQLite");
+ SQLiteDataSource sqliteDataSource = new SQLiteDataSource();
+ sqliteDataSource.setUrl("jdbc:sqlite:file:" + sqliteDb.toFile().getAbsolutePath());
+ dataSource = sqliteDataSource;
+
+ queryProvider = SQLiteVectorStoreQueryProvider.builder()
+ .withDataSource(dataSource)
+ .build();
+ break;
+ case HSQLDB:
+ try {
+ Path file = createTempDbFile("testHSQLDB");
+
+ Properties properties = new Properties();
+ properties.putAll(
+ Map.of(
+ "url", "jdbc:hsqldb:file:" + file.toFile().getAbsolutePath()
+ + ";sql.syntax_mys=true",
+ "user", "SA",
+ "password", ""
+ )
+ );
+
+ dataSource = JDBCDataSourceFactory.createDataSource(properties);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ queryProvider = HSQLDBVectorStoreQueryProvider.builder()
+ .withDataSource(dataSource)
+ .build();
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown query provider: " + provider);
+ }
+
+ JDBCVectorStore vectorStore = JDBCVectorStore.builder()
+ .withDataSource(dataSource)
+ .withOptions(
+ JDBCVectorStoreOptions.builder()
+ .withQueryProvider(queryProvider)
+ .build()
+ )
+ .build();
+
+ vectorStore.prepareAsync().block();
+ return vectorStore;
+ }
+
+
+ @ParameterizedTest
+ @EnumSource(QueryProvider.class)
+ public void getCollectionNamesAsync(QueryProvider provider) {
+ JDBCVectorStore vectorStore = buildVectorStore(provider);
+
+ vectorStore.getCollectionNamesAsync().block();
+
+ List collectionNames = Arrays.asList("collection1", "collection2", "collection3");
+
+ for (String collectionName : collectionNames) {
+ vectorStore.getCollection(collectionName,
+ JDBCVectorStoreRecordCollectionOptions.builder()
+ .withRecordClass(Hotel.class)
+ .build()).createCollectionAsync().block();
+ }
+
+ List retrievedCollectionNames = vectorStore.getCollectionNamesAsync().block();
+ assertNotNull(retrievedCollectionNames);
+ assertEquals(collectionNames.size(), retrievedCollectionNames.size());
+ for (String collectionName : collectionNames) {
+ assertTrue(retrievedCollectionNames.contains(collectionName));
+ }
+ }
+}
diff --git a/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/Hotel.java b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/Hotel.java
new file mode 100644
index 000000000..46083425a
--- /dev/null
+++ b/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/data/redis/Hotel.java
@@ -0,0 +1,102 @@
+package com.microsoft.semantickernel.tests.data.redis;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordData;
+import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordKey;
+import com.microsoft.semantickernel.data.vectorstorage.annotations.VectorStoreRecordVector;
+import com.microsoft.semantickernel.data.vectorstorage.definition.DistanceFunction;
+import com.microsoft.semantickernel.data.vectorstorage.definition.IndexKind;
+
+import java.util.List;
+
+public class Hotel {
+
+ @VectorStoreRecordKey
+ private final String id;
+
+ @VectorStoreRecordData(isFilterable = true)
+ private final String name;
+
+ @VectorStoreRecordData(isFilterable = true)
+ private final int code;
+
+ @JsonProperty("summary")
+ @VectorStoreRecordData(isFilterable = true)
+ private final String description;
+
+ @JsonProperty("summaryEmbedding1")
+ @VectorStoreRecordVector(dimensions = 8, indexKind = IndexKind.HNSW, distanceFunction = DistanceFunction.EUCLIDEAN_DISTANCE)
+ private final List euclidean;
+
+ @JsonProperty("summaryEmbedding2")
+ @VectorStoreRecordVector(dimensions = 8)
+ private final List