mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Merge branch 'java-v1' into java-development
This commit is contained in:
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
+20
-98
@@ -34,121 +34,43 @@ A BOM is provided that can be used to define the versions of all Semantic Kernel
|
||||
`semantickernel-api`
|
||||
: Package that defines the core public API for the Semantic Kernel for a Maven project.
|
||||
|
||||
`semantickernel-core`
|
||||
: The internal implementation of the Semantic Kernel API.
|
||||
This package contains the core implementation of the Semantic Kernel. This must be made available to the
|
||||
application at runtime. Note that these classes are considered internal and should not be used directly by the application. Should they be used directly, it could cause instability in the application as they may change without notice over time.
|
||||
## Services
|
||||
|
||||
## Connectors
|
||||
|
||||
`semantickernel-connectors-ai-openai`
|
||||
`semantickernel-aiservices-openai`
|
||||
: Provides a connector that can be used to interact with the OpenAI API.
|
||||
|
||||
### Memory Connectors
|
||||
|
||||
#### JDBC Memory Connectors
|
||||
|
||||
`semantickernel-connectors-memory-jdbc`
|
||||
: A package that serves as a parent for all JDBC based connectors, adding a new JDBC database should be a matter of extending the classes within this module.
|
||||
|
||||
Provides a memory connector that can be used to interact with a JDBC database.
|
||||
- `semantickernel-connectors-memory-sqlite`
|
||||
- `semantickernel-connectors-memory-postgresql`
|
||||
- `semantickernel-connectors-memory-mysql`
|
||||
|
||||
`semantickernel-connectors-memory-azurecognitivesearch`
|
||||
: Provides a memory connector for using Azure Cognitive Search as the memory provider for an application.
|
||||
|
||||
### Miscellaneous Packages
|
||||
|
||||
`semantickernel-planners`
|
||||
: Implementations of various planners that can be used in the execution of semantic functions.
|
||||
|
||||
`semantickernel-plugin-core`
|
||||
: Several example plugins, many of which are used in the [Semantic Kernel for Java samples](./samples).
|
||||
|
||||
`semantickernel-gpt3-tokenizer`
|
||||
: A tokenizer that can be used to tokenize text for use with GPT-3. Can be used to estimate cost.
|
||||
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Example: OpenAI + SQLite
|
||||
|
||||
POM XML for a simple project that uses OpenAI for text or chat completion with an SQLite database as the memory provider.
|
||||
POM XML for a simple project that uses OpenAI.
|
||||
|
||||
```xml
|
||||
<dependencyManagement>
|
||||
|
||||
<project>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-bom</artifactId>
|
||||
<version>${semantickernel.version}</version>
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-bom</artifactId>
|
||||
<version>${semantickernel.version}</version>
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-core</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-ai-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-sqlite</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
### Example: Planner using Open AI + Cognitive Search
|
||||
|
||||
The POM XML content for a project that uses Open AI for planning, using Azure Cognitive Search as the memory provider.
|
||||
|
||||
```xml
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-bom</artifactId>
|
||||
<version>${semantickernel.version}</version>
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
<artifactId>semantickernel-connectors-ai-openai</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-core</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-ai-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-azurecognitivesearch</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-planners</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
+29
-13
@@ -1,15 +1,24 @@
|
||||
# Semantic Kernel for Java
|
||||
|
||||
Semantic Kernel (SK) is a lightweight foundation that lets you easily mix conventional programming languages with the latest in
|
||||
Semantic Kernel (SK) is a lightweight foundation that lets you easily mix conventional programming languages with the
|
||||
latest in
|
||||
Large Language Model (LLM) AI "prompts" with templating, chaining, and planning capabilities out-of-the-box.
|
||||
|
||||
To learn more about Microsoft Semantic Kernel, visit
|
||||
the [Microsoft Semantic Kernel documentation](https://learn.microsoft.com/en-us/semantic-kernel/whatissk).
|
||||
|
||||
The Microsoft Semantic Kernel for Java is a library that implements the key concepts and foundations of Microsoft Semantic Kernel. It is designed
|
||||
to be used in Java applications in both client (desktop, mobile, CLIs) and server environments in an idiomatic way, and to be easily integrated with other Java libraries
|
||||
The Microsoft Semantic Kernel for Java is a library that implements the key concepts and foundations of Microsoft
|
||||
Semantic Kernel. It is designed
|
||||
to be used in Java applications in both client (desktop, mobile, CLIs) and server environments in an idiomatic way, and
|
||||
to be easily integrated with other Java libraries
|
||||
and frameworks.
|
||||
|
||||
## Quickstart
|
||||
|
||||
To get an idea of how to use the Semantic Kernel for Java, you can check
|
||||
the [syntax examples](samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples) folder for
|
||||
examples of common AI-enabled scenarios.
|
||||
|
||||
## Get started
|
||||
|
||||
To run the LLM prompts and semantic functions in this kernel, make sure you have
|
||||
@@ -21,24 +30,31 @@ or [Azure Open AI service key](https://learn.microsoft.com/azure/cognitive-servi
|
||||
To build the Semantic Kernel for Java, you will need:
|
||||
|
||||
- **Required**:
|
||||
- [OpenJDK 17](https://microsoft.com/openjdk/) or newer
|
||||
- [OpenJDK 17](https://microsoft.com/openjdk/) or newer
|
||||
|
||||
### Build the Semantic Kernel
|
||||
|
||||
1. Clone this repository
|
||||
|
||||
git clone -b java-development https://github.com/microsoft/semantic-kernel/
|
||||
git clone -b java-v1 https://github.com/microsoft/semantic-kernel/
|
||||
|
||||
2. Build the project with the Maven Wrapper
|
||||
|
||||
cd semantic-kernel/java
|
||||
./mvnw install
|
||||
|
||||
3. (Optional) To run a FULL build including static analysis and end-to-end tests that might require a valid OpenAI key,
|
||||
run the following command:
|
||||
|
||||
./mvnw clean install -Prelease,bug-check,with-samples
|
||||
|
||||
## Using the Semantic Kernel for Java
|
||||
|
||||
The library is organized in a set of dependencies published to Maven Central. For a list of the Maven dependencies and how to use each of them, see [PACKAGES.md](PACKAGES.md).
|
||||
The library is organized in a set of dependencies published to Maven Central. For a list of the Maven dependencies and
|
||||
how to use each of them, see [PACKAGES.md](PACKAGES.md).
|
||||
|
||||
Alternatively, check the `samples` folder for examples of common AI-enabled scenarios implemented with Semantic Kernel for Java.
|
||||
Alternatively, check the `samples` folder for examples of common AI-enabled scenarios implemented with Semantic Kernel
|
||||
for Java.
|
||||
|
||||
## Discord community
|
||||
|
||||
@@ -52,13 +68,13 @@ and get help from the community. We have a `#java` channel for Java-specific que
|
||||
The project may contain end-to-end tests that require an OpenAI key to run. To run these tests locally, you
|
||||
will need to set the following environment variable:
|
||||
|
||||
- `OPENAI_API_KEY` - the OpenAI API key.
|
||||
- `CLIENT_KEY` - the OpenAI API key.
|
||||
|
||||
If you are using Azure OpenAI, you will also need to set the following environment variables:
|
||||
|
||||
- `AZURE_OPENAI_ENDPOINT` - the Azure OpenAI endpoint found in **Keys * Endpoint** section of the Azure OpenAI service.
|
||||
- `AZURE_OPENAI_API_KEY` - the Azure OpenAI API key found in **Keys * Endpoint** section of the Azure OpenAI service.
|
||||
- `AZURE_OPENAI_DEPLOYMENT_NAME` - the custom name you chose for your deployment when you deployed a model. It can be
|
||||
- `CLIENT_ENDPOINT` - the Azure OpenAI endpoint found in **Keys * Endpoint** section of the Azure OpenAI service.
|
||||
- `AZURE_CLIENT_KEY` - the Azure OpenAI API key found in **Keys * Endpoint** section of the Azure OpenAI service.
|
||||
- `MODEL_ID` - the custom name you chose for your deployment when you deployed a model. It can be
|
||||
found under **Resource Management > Deployments** in the Azure Portal.
|
||||
|
||||
For more information, see the Azure OpenAI documentation
|
||||
@@ -70,7 +86,7 @@ To run the unit tests only, run the following command:
|
||||
|
||||
To run all tests, including integration tests that require an OpenAI key, run the following command:
|
||||
|
||||
./mvnw verify
|
||||
./mvnw verify -Prelease,bug-check,with-samples
|
||||
|
||||
### Submitting a pull request
|
||||
|
||||
@@ -88,7 +104,7 @@ Also ensure that:
|
||||
- All new code is covered by unit tests
|
||||
- All new code is covered by integration tests
|
||||
|
||||
Once your proposal is ready, submit a pull request to the `java-development` branch. The pull request will be reviewed by the
|
||||
Once your proposal is ready, submit a pull request to the `java-v1` branch. The pull request will be reviewed by the
|
||||
project maintainers.
|
||||
|
||||
Make sure your pull request has an objective title and a clear description explaining the problem and solution.
|
||||
|
||||
+29
-16
@@ -1,26 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<version>1.0.3-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Semantic Kernel GPT3 Tokenizer Extension</name>
|
||||
<description>GPT3 Tokenizer for Semantic Kernel</description>
|
||||
<artifactId>semantickernel-gpt3-tokenizer</artifactId>
|
||||
<artifactId>semantickernel-aiservices-openai</artifactId>
|
||||
<name>Semantic Kernel OpenAI Services</name>
|
||||
<description>OpenAI services for Semantic Kernel</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
<groupId>com.azure</groupId>
|
||||
<artifactId>azure-ai-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
@@ -28,14 +29,26 @@
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Required for Android -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
<groupId>javax.xml.stream</groupId>
|
||||
<artifactId>stax-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.microsoft.semantickernel.services.AIService;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Provides OpenAI service.
|
||||
*/
|
||||
public abstract class OpenAiService implements AIService {
|
||||
|
||||
private final OpenAIAsyncClient client;
|
||||
@Nullable
|
||||
private final String serviceId;
|
||||
private final String modelId;
|
||||
|
||||
protected OpenAiService(OpenAIAsyncClient client, @Nullable String serviceId, String modelId) {
|
||||
this.client = client;
|
||||
this.serviceId = serviceId;
|
||||
this.modelId = modelId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getModelId() {
|
||||
return modelId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
protected OpenAIAsyncClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.audio;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.models.AudioTranscription;
|
||||
import com.azure.ai.openai.models.AudioTranscriptionFormat;
|
||||
import com.azure.ai.openai.models.AudioTranscriptionOptions;
|
||||
import com.microsoft.semantickernel.aiservices.openai.OpenAiService;
|
||||
import com.microsoft.semantickernel.exceptions.SKException;
|
||||
import com.microsoft.semantickernel.services.audio.AudioContent;
|
||||
import com.microsoft.semantickernel.services.audio.AudioToTextExecutionSettings;
|
||||
import com.microsoft.semantickernel.services.audio.AudioToTextService;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Provides OpenAi implementation of audio to text service.
|
||||
*/
|
||||
public class OpenAiAudioToTextService extends OpenAiService implements AudioToTextService {
|
||||
|
||||
/**
|
||||
* Creates an instance of OpenAi audio to text service.
|
||||
*
|
||||
* @param client OpenAI client.
|
||||
* @param modelId The model ID.
|
||||
*/
|
||||
public OpenAiAudioToTextService(
|
||||
OpenAIAsyncClient client,
|
||||
String modelId) {
|
||||
super(client, null, modelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> getTextContentsAsync(
|
||||
AudioContent content,
|
||||
@Nullable AudioToTextExecutionSettings executionSettings) {
|
||||
|
||||
AudioTranscriptionOptions options = convertOptions(content, executionSettings);
|
||||
|
||||
// TODO: Should use getAudioTranscriptionTextWithResponse, and OpenAIRequestSettings.getRequestOptions()
|
||||
// however currently this breaks the request
|
||||
return getClient()
|
||||
.getAudioTranscription(
|
||||
getModelId(),
|
||||
options.getFilename(),
|
||||
options)
|
||||
.map(AudioTranscription::getText);
|
||||
}
|
||||
|
||||
private AudioTranscriptionOptions convertOptions(
|
||||
AudioContent content,
|
||||
@Nullable AudioToTextExecutionSettings executionSettings) {
|
||||
AudioTranscriptionOptions options = new AudioTranscriptionOptions(content.getData());
|
||||
|
||||
options.setModel(getModelId());
|
||||
if (executionSettings == null) {
|
||||
return options;
|
||||
}
|
||||
|
||||
if (executionSettings.getResponseFormat() != null) {
|
||||
options.setResponseFormat(
|
||||
AudioTranscriptionFormat.fromString(executionSettings.getResponseFormat()));
|
||||
}
|
||||
|
||||
if (executionSettings.getFilename() != null) {
|
||||
options.setFilename(executionSettings.getFilename());
|
||||
}
|
||||
|
||||
if (executionSettings.getLanguage() != null) {
|
||||
options.setLanguage(executionSettings.getLanguage());
|
||||
}
|
||||
|
||||
if (executionSettings.getPrompt() != null) {
|
||||
options.setPrompt(executionSettings.getPrompt());
|
||||
}
|
||||
|
||||
if (executionSettings.getTemperature() != null) {
|
||||
options.setTemperature(executionSettings.getTemperature());
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for OpenAiAudioToTextService.
|
||||
*
|
||||
* @return The builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a builder for OpenAiAudioToTextService.
|
||||
*/
|
||||
public static class Builder extends AudioToTextService.Builder {
|
||||
|
||||
/**
|
||||
* builds the OpenAiAudioToTextService.
|
||||
*/
|
||||
@Override
|
||||
public AudioToTextService build() {
|
||||
if (client == null) {
|
||||
throw new SKException("OpenAI client is required");
|
||||
}
|
||||
if (modelId == null) {
|
||||
throw new SKException("Model id is required");
|
||||
}
|
||||
return new OpenAiAudioToTextService(client, modelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.audio;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.models.SpeechGenerationOptions;
|
||||
import com.azure.ai.openai.models.SpeechGenerationResponseFormat;
|
||||
import com.azure.ai.openai.models.SpeechVoice;
|
||||
import com.microsoft.semantickernel.aiservices.openai.OpenAiService;
|
||||
import com.microsoft.semantickernel.exceptions.SKException;
|
||||
import com.microsoft.semantickernel.services.audio.AudioContent;
|
||||
import com.microsoft.semantickernel.services.audio.TextToAudioExecutionSettings;
|
||||
import com.microsoft.semantickernel.services.audio.TextToAudioService;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Provides OpenAi implementation of text to audio service.
|
||||
*/
|
||||
public class OpenAiTextToAudioService extends OpenAiService implements TextToAudioService {
|
||||
|
||||
/**
|
||||
* Creates an instance of OpenAi text to audio service.
|
||||
*
|
||||
* @param client OpenAI client.
|
||||
* @param modelId The model ID.
|
||||
*/
|
||||
public OpenAiTextToAudioService(
|
||||
OpenAIAsyncClient client,
|
||||
String modelId) {
|
||||
super(client, null, modelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<AudioContent> getAudioContentAsync(
|
||||
String text,
|
||||
TextToAudioExecutionSettings executionSettings) {
|
||||
|
||||
SpeechGenerationOptions options = convertOptions(text, executionSettings);
|
||||
|
||||
return getClient().generateSpeechFromText(getModelId(), options)
|
||||
.map(response -> new AudioContent(response.toBytes(), getModelId()));
|
||||
}
|
||||
|
||||
private SpeechGenerationOptions convertOptions(
|
||||
String text,
|
||||
TextToAudioExecutionSettings executionSettings) {
|
||||
SpeechGenerationOptions options = new SpeechGenerationOptions(
|
||||
text,
|
||||
SpeechVoice.fromString(executionSettings.getVoice()));
|
||||
|
||||
options.setModel(getModelId());
|
||||
|
||||
if (executionSettings.getResponseFormat() != null) {
|
||||
options.setResponseFormat(
|
||||
SpeechGenerationResponseFormat.fromString(executionSettings.getResponseFormat()));
|
||||
}
|
||||
|
||||
if (executionSettings.getSpeed() != null) {
|
||||
options.setSpeed(executionSettings.getSpeed());
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder.
|
||||
*
|
||||
* @return The builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a builder for OpenAi text to audio service.
|
||||
*/
|
||||
|
||||
public static class Builder extends TextToAudioService.Builder {
|
||||
|
||||
/**
|
||||
* Builds the OpenAi text to audio service.
|
||||
*
|
||||
* @return The OpenAi text to audio service.
|
||||
*/
|
||||
@Override
|
||||
public TextToAudioService build() {
|
||||
if (client == null) {
|
||||
throw new SKException("OpenAI client is required");
|
||||
}
|
||||
if (modelId == null) {
|
||||
throw new SKException("Model id is required");
|
||||
}
|
||||
return new OpenAiTextToAudioService(client, modelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+752
@@ -0,0 +1,752 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.models.ChatChoice;
|
||||
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.ChatCompletionsJsonResponseFormat;
|
||||
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.ChatRequestAssistantMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestSystemMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestToolMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestUserMessage;
|
||||
import com.azure.ai.openai.models.ChatResponseMessage;
|
||||
import com.azure.ai.openai.models.FunctionCall;
|
||||
import com.azure.core.util.BinaryData;
|
||||
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.implementation.OpenAIRequestSettings;
|
||||
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.SKException;
|
||||
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.orchestration.FunctionResult;
|
||||
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
|
||||
import com.microsoft.semantickernel.orchestration.InvocationContext;
|
||||
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
|
||||
import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* OpenAI chat completion service.
|
||||
*/
|
||||
public class OpenAIChatCompletion extends OpenAiService implements ChatCompletionService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAIChatCompletion.class);
|
||||
|
||||
protected OpenAIChatCompletion(
|
||||
OpenAIAsyncClient client,
|
||||
String modelId,
|
||||
@Nullable String serviceId) {
|
||||
super(client, serviceId, modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link OpenAIChatCompletion.Builder}.
|
||||
*
|
||||
* @return a new instance of {@link OpenAIChatCompletion.Builder}
|
||||
*/
|
||||
public static OpenAIChatCompletion.Builder builder() {
|
||||
return new OpenAIChatCompletion.Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<ChatMessageContent<?>>> getChatMessageContentsAsync(
|
||||
ChatHistory chatHistory,
|
||||
@Nullable Kernel kernel,
|
||||
@Nullable InvocationContext invocationContext) {
|
||||
|
||||
List<ChatRequestMessage> chatRequestMessages = getChatRequestMessages(chatHistory);
|
||||
return internalChatMessageContentsAsync(
|
||||
chatRequestMessages,
|
||||
kernel,
|
||||
invocationContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<ChatMessageContent<?>>> getChatMessageContentsAsync(
|
||||
String prompt,
|
||||
@Nullable Kernel kernel,
|
||||
@Nullable InvocationContext invocationContext) {
|
||||
ParsedPrompt parsedPrompt = XMLPromptParser.parse(prompt);
|
||||
|
||||
return internalChatMessageContentsAsync(
|
||||
parsedPrompt.getChatRequestMessages(),
|
||||
kernel,
|
||||
invocationContext);
|
||||
}
|
||||
|
||||
private Mono<List<ChatMessageContent<?>>> internalChatMessageContentsAsync(
|
||||
List<ChatRequestMessage> messages,
|
||||
@Nullable Kernel kernel,
|
||||
@Nullable InvocationContext invocationContext) {
|
||||
|
||||
List<OpenAIFunction> functions = new ArrayList<>();
|
||||
if (kernel != null) {
|
||||
kernel.getPlugins()
|
||||
.forEach(plugin -> plugin.getFunctions().forEach((name, function) -> functions
|
||||
.add(OpenAIFunction.build(function.getMetadata(), plugin.getName()))));
|
||||
}
|
||||
|
||||
// Create copy to avoid reactor exceptions when updating request messages internally
|
||||
return internalChatMessageContentsAsync(
|
||||
new ArrayList<>(messages),
|
||||
kernel,
|
||||
functions,
|
||||
invocationContext,
|
||||
Math.min(MAXIMUM_INFLIGHT_AUTO_INVOKES,
|
||||
invocationContext != null && invocationContext.getToolCallBehavior() != null
|
||||
? invocationContext.getToolCallBehavior().getMaximumAutoInvokeAttempts()
|
||||
: 0));
|
||||
}
|
||||
|
||||
private Mono<List<ChatMessageContent<?>>> internalChatMessageContentsAsync(
|
||||
List<ChatRequestMessage> messages,
|
||||
@Nullable Kernel kernel,
|
||||
List<OpenAIFunction> functions,
|
||||
@Nullable InvocationContext invocationContext,
|
||||
int autoInvokeAttempts) {
|
||||
|
||||
ChatCompletionsOptions options = executeHook(
|
||||
invocationContext,
|
||||
new PreChatCompletionEvent(
|
||||
getCompletionsOptions(
|
||||
this,
|
||||
messages,
|
||||
functions,
|
||||
invocationContext)))
|
||||
.getOptions();
|
||||
|
||||
Mono<List<? extends ChatMessageContent>> result = getClient()
|
||||
.getChatCompletionsWithResponse(getModelId(), options,
|
||||
OpenAIRequestSettings.getRequestOptions())
|
||||
.flatMap(completionsResult -> {
|
||||
if (completionsResult.getStatusCode() >= 400) {
|
||||
return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR,
|
||||
"Request failed: " + completionsResult.getStatusCode()));
|
||||
}
|
||||
return Mono.just(completionsResult.getValue());
|
||||
})
|
||||
.flatMap(completions -> {
|
||||
List<ChatResponseMessage> responseMessages = completions
|
||||
.getChoices()
|
||||
.stream()
|
||||
.map(ChatChoice::getMessage)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// execute post chat completion hook
|
||||
executeHook(invocationContext, new PostChatCompletionEvent(completions));
|
||||
|
||||
// Just return the result:
|
||||
// If we don't want to attempt to invoke any functions
|
||||
// Or if we are auto-invoking, but we somehow end up with other than 1 choice even though only 1 was requested
|
||||
if (autoInvokeAttempts == 0 || responseMessages.size() != 1) {
|
||||
return getChatMessageContentsAsync(completions);
|
||||
}
|
||||
// Or if there are no tool calls to be done
|
||||
ChatResponseMessage response = responseMessages.get(0);
|
||||
List<ChatCompletionsToolCall> toolCalls = response.getToolCalls();
|
||||
if (toolCalls == null || toolCalls.isEmpty()) {
|
||||
return getChatMessageContentsAsync(completions);
|
||||
}
|
||||
|
||||
ChatRequestAssistantMessage requestMessage = new ChatRequestAssistantMessage(
|
||||
response.getContent());
|
||||
requestMessage.setToolCalls(toolCalls);
|
||||
|
||||
// Add the original assistant message to the chat options; this is required for the service
|
||||
// to understand the tool call responses
|
||||
messages.add(requestMessage);
|
||||
|
||||
return Flux
|
||||
.fromIterable(toolCalls)
|
||||
.reduce(
|
||||
Mono.just(messages),
|
||||
(requestMessages, toolCall) -> {
|
||||
if (toolCall instanceof ChatCompletionsFunctionToolCall) {
|
||||
return performToolCall(kernel, invocationContext, requestMessages,
|
||||
toolCall);
|
||||
}
|
||||
|
||||
return requestMessages;
|
||||
})
|
||||
.flatMap(it -> it)
|
||||
.flatMap(msgs -> {
|
||||
return internalChatMessageContentsAsync(msgs, kernel, functions,
|
||||
invocationContext, autoInvokeAttempts - 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) {
|
||||
List<ChatRequestMessage> currentMessages = messages;
|
||||
if (e instanceof FunctionInvocationError) {
|
||||
currentMessages = ((FunctionInvocationError) e).getMessages();
|
||||
}
|
||||
return internalChatMessageContentsAsync(currentMessages, kernel,
|
||||
functions,
|
||||
invocationContext, autoInvokeAttempts - 1);
|
||||
} else {
|
||||
return Mono.error(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return result.map(op -> (List<ChatMessageContent<?>>) op);
|
||||
}
|
||||
|
||||
private Mono<List<ChatRequestMessage>> performToolCall(
|
||||
@Nullable Kernel kernel,
|
||||
@Nullable InvocationContext invocationContext,
|
||||
Mono<List<ChatRequestMessage>> requestMessages,
|
||||
ChatCompletionsToolCall toolCall) {
|
||||
return requestMessages
|
||||
.flatMap(msgs -> {
|
||||
try {
|
||||
// OpenAI only supports function tool call at the moment
|
||||
ChatCompletionsFunctionToolCall functionToolCall = (ChatCompletionsFunctionToolCall) toolCall;
|
||||
if (kernel == null) {
|
||||
return Mono
|
||||
.error(new SKException(
|
||||
"A tool call was requested, but no kernel was provided to the invocation, this is a unsupported configuration"));
|
||||
}
|
||||
|
||||
ContextVariableTypes contextVariableTypes = invocationContext == null
|
||||
? new ContextVariableTypes()
|
||||
: invocationContext.getContextVariableTypes();
|
||||
|
||||
return invokeFunctionTool(
|
||||
kernel,
|
||||
invocationContext,
|
||||
functionToolCall,
|
||||
contextVariableTypes)
|
||||
.map(functionResult -> {
|
||||
// Add chat request tool message to the chat options
|
||||
ChatRequestMessage requestToolMessage = new ChatRequestToolMessage(
|
||||
functionResult.getResult(),
|
||||
functionToolCall.getId());
|
||||
|
||||
ArrayList<ChatRequestMessage> res = new ArrayList<>(msgs);
|
||||
res.add(requestToolMessage);
|
||||
|
||||
return res;
|
||||
})
|
||||
.switchIfEmpty(Mono.fromSupplier(
|
||||
() -> {
|
||||
ChatRequestMessage requestToolMessage = new ChatRequestToolMessage(
|
||||
"Completed successfully with no return value",
|
||||
functionToolCall.getId());
|
||||
|
||||
ArrayList<ChatRequestMessage> res = new ArrayList<>(msgs);
|
||||
res.add(requestToolMessage);
|
||||
return res;
|
||||
}))
|
||||
.onErrorResume(e -> emitError(toolCall, msgs, e));
|
||||
} catch (Exception e) {
|
||||
return emitError(toolCall, msgs, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<ArrayList<ChatRequestMessage>> emitError(
|
||||
ChatCompletionsToolCall toolCall,
|
||||
List<ChatRequestMessage> msgs,
|
||||
Throwable e) {
|
||||
|
||||
msgs.add(new ChatRequestToolMessage(
|
||||
"Call failed: " + e.getMessage(),
|
||||
toolCall.getId()));
|
||||
|
||||
return Mono.error(
|
||||
new FunctionInvocationError(e, msgs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception to be thrown when a function invocation fails.
|
||||
*/
|
||||
private static class FunctionInvocationError extends SKException {
|
||||
|
||||
private final List<ChatRequestMessage> messages;
|
||||
|
||||
public FunctionInvocationError(Throwable e, List<ChatRequestMessage> msgs) {
|
||||
super(e.getMessage(), e);
|
||||
this.messages = msgs;
|
||||
}
|
||||
|
||||
public List<ChatRequestMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("StringSplitter")
|
||||
private Mono<FunctionResult<String>> invokeFunctionTool(
|
||||
Kernel kernel,
|
||||
@Nullable InvocationContext invocationContext,
|
||||
ChatCompletionsFunctionToolCall toolCall,
|
||||
ContextVariableTypes contextVariableTypes) {
|
||||
|
||||
try {
|
||||
OpenAIFunctionToolCall openAIFunctionToolCall = extractOpenAIFunctionToolCall(toolCall);
|
||||
String pluginName = openAIFunctionToolCall.getPluginName();
|
||||
if (pluginName == null || pluginName.isEmpty()) {
|
||||
return Mono.error(
|
||||
new SKException("Plugin name is required for function tool call"));
|
||||
}
|
||||
|
||||
KernelFunction<?> function = kernel.getFunction(
|
||||
pluginName,
|
||||
openAIFunctionToolCall.getFunctionName());
|
||||
|
||||
PreToolCallEvent hookResult = executeHook(invocationContext, new PreToolCallEvent(
|
||||
openAIFunctionToolCall.getFunctionName(),
|
||||
openAIFunctionToolCall.getArguments(),
|
||||
function,
|
||||
contextVariableTypes));
|
||||
|
||||
function = hookResult.getFunction();
|
||||
KernelFunctionArguments arguments = hookResult.getArguments();
|
||||
|
||||
return function
|
||||
.invokeAsync(kernel)
|
||||
.withArguments(arguments)
|
||||
.withResultType(contextVariableTypes.getVariableTypeForClass(String.class));
|
||||
} catch (JsonProcessingException e) {
|
||||
return Mono.error(new SKException("Failed to parse tool arguments"));
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends KernelHookEvent> T executeHook(
|
||||
@Nullable InvocationContext invocationContext,
|
||||
T event) {
|
||||
KernelHooks kernelHooks = invocationContext != null
|
||||
&& invocationContext.getKernelHooks() != null
|
||||
? invocationContext.getKernelHooks()
|
||||
: new KernelHooks();
|
||||
|
||||
return kernelHooks.executeHooks(event);
|
||||
}
|
||||
|
||||
@SuppressWarnings("StringSplitter")
|
||||
private OpenAIFunctionToolCall extractOpenAIFunctionToolCall(
|
||||
ChatCompletionsFunctionToolCall toolCall) throws JsonProcessingException {
|
||||
|
||||
// Split the full name of a function into plugin and function name
|
||||
String name = toolCall.getFunction().getName();
|
||||
String[] parts = name.split(OpenAIFunction.getNameSeparator());
|
||||
String pluginName = parts.length > 1 ? parts[0] : "";
|
||||
String fnName = parts.length > 1 ? parts[1] : parts[0];
|
||||
|
||||
KernelFunctionArguments arguments = KernelFunctionArguments.builder().build();
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode jsonToolCallArguments = mapper.readTree(toolCall.getFunction().getArguments());
|
||||
|
||||
jsonToolCallArguments.fields().forEachRemaining(
|
||||
entry -> {
|
||||
if (entry.getValue() instanceof ContainerNode) {
|
||||
arguments.put(entry.getKey(),
|
||||
ContextVariable.of(entry.getValue().toPrettyString()));
|
||||
} else {
|
||||
arguments.put(entry.getKey(),
|
||||
ContextVariable.of(entry.getValue().asText()));
|
||||
}
|
||||
});
|
||||
|
||||
return new OpenAIFunctionToolCall(
|
||||
toolCall.getId(),
|
||||
pluginName,
|
||||
fnName,
|
||||
arguments);
|
||||
|
||||
}
|
||||
|
||||
private Mono<List<OpenAIChatMessageContent>> getChatMessageContentsAsync(
|
||||
ChatCompletions completions) {
|
||||
FunctionResultMetadata completionMetadata = FunctionResultMetadata.build(
|
||||
completions.getId(),
|
||||
completions.getUsage(),
|
||||
completions.getCreatedAt());
|
||||
|
||||
List<ChatResponseMessage> responseMessages = completions
|
||||
.getChoices()
|
||||
.stream()
|
||||
.map(ChatChoice::getMessage)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Flux.fromIterable(responseMessages)
|
||||
.map(response -> new OpenAIChatMessageContent(
|
||||
AuthorRole.ASSISTANT,
|
||||
response.getContent(),
|
||||
this.getModelId(),
|
||||
null,
|
||||
null,
|
||||
completionMetadata,
|
||||
formOpenAiToolCalls(response)))
|
||||
.collectList();
|
||||
}
|
||||
|
||||
private List<OpenAIFunctionToolCall> formOpenAiToolCalls(ChatResponseMessage response) {
|
||||
if (response.getToolCalls() == null || response.getToolCalls().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return response
|
||||
.getToolCalls()
|
||||
.stream()
|
||||
.map(call -> {
|
||||
if (call instanceof ChatCompletionsFunctionToolCall) {
|
||||
try {
|
||||
return extractOpenAIFunctionToolCall(
|
||||
(ChatCompletionsFunctionToolCall) call);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new SKException("Failed to parse tool arguments", e);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static ChatCompletionsOptions getCompletionsOptions(
|
||||
ChatCompletionService chatCompletionService,
|
||||
List<ChatRequestMessage> chatRequestMessages,
|
||||
@Nullable List<OpenAIFunction> functions,
|
||||
@Nullable InvocationContext invocationContext) {
|
||||
|
||||
chatRequestMessages = chatRequestMessages
|
||||
.stream()
|
||||
.map(XMLPromptParser::unescapeRequest)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ChatCompletionsOptions options = new ChatCompletionsOptions(chatRequestMessages)
|
||||
.setModel(chatCompletionService.getModelId());
|
||||
|
||||
if (invocationContext != null && invocationContext.getToolCallBehavior() != null) {
|
||||
configureToolCallBehaviorOptions(
|
||||
options,
|
||||
invocationContext.getToolCallBehavior(),
|
||||
functions,
|
||||
chatRequestMessages);
|
||||
}
|
||||
|
||||
PromptExecutionSettings promptExecutionSettings = invocationContext != null
|
||||
? invocationContext.getPromptExecutionSettings()
|
||||
: null;
|
||||
|
||||
if (promptExecutionSettings == null) {
|
||||
return options;
|
||||
}
|
||||
|
||||
if (promptExecutionSettings.getResultsPerPrompt() < 1
|
||||
|| promptExecutionSettings.getResultsPerPrompt() > MAX_RESULTS_PER_PROMPT) {
|
||||
throw new AIException(AIException.ErrorCodes.INVALID_REQUEST,
|
||||
String.format("Results per prompt must be in range between 1 and %d, inclusive.",
|
||||
MAX_RESULTS_PER_PROMPT));
|
||||
}
|
||||
|
||||
Map<String, Integer> logit = null;
|
||||
if (promptExecutionSettings.getTokenSelectionBiases() != null) {
|
||||
logit = promptExecutionSettings
|
||||
.getTokenSelectionBiases()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
entry -> entry.getKey().toString(),
|
||||
Map.Entry::getValue));
|
||||
}
|
||||
|
||||
options
|
||||
.setTemperature(promptExecutionSettings.getTemperature())
|
||||
.setTopP(promptExecutionSettings.getTopP())
|
||||
.setPresencePenalty(promptExecutionSettings.getPresencePenalty())
|
||||
.setFrequencyPenalty(promptExecutionSettings.getFrequencyPenalty())
|
||||
.setPresencePenalty(promptExecutionSettings.getPresencePenalty())
|
||||
.setMaxTokens(promptExecutionSettings.getMaxTokens())
|
||||
.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())
|
||||
.setUser(promptExecutionSettings.getUser())
|
||||
.setLogitBias(logit);
|
||||
|
||||
if (promptExecutionSettings.getResponseFormat() != null) {
|
||||
switch (promptExecutionSettings.getResponseFormat()) {
|
||||
case JSON_OBJECT:
|
||||
options.setResponseFormat(new ChatCompletionsJsonResponseFormat());
|
||||
break;
|
||||
case TEXT:
|
||||
options.setResponseFormat(new ChatCompletionsTextResponseFormat());
|
||||
break;
|
||||
default:
|
||||
throw new SKException(
|
||||
"Unknown response format: " + promptExecutionSettings.getResponseFormat());
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static void configureToolCallBehaviorOptions(
|
||||
ChatCompletionsOptions options,
|
||||
@Nullable ToolCallBehavior toolCallBehavior,
|
||||
@Nullable List<OpenAIFunction> functions,
|
||||
List<ChatRequestMessage> chatRequestMessages) {
|
||||
|
||||
if (toolCallBehavior == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (functions == null || functions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a specific function is required to be called
|
||||
if (toolCallBehavior instanceof ToolCallBehavior.RequiredKernelFunction) {
|
||||
KernelFunction<?> toolChoice = ((ToolCallBehavior.RequiredKernelFunction) toolCallBehavior)
|
||||
.getRequiredFunction();
|
||||
|
||||
String toolChoiceName = String.format("%s%s%s",
|
||||
toolChoice.getPluginName(),
|
||||
OpenAIFunction.getNameSeparator(),
|
||||
toolChoice.getName());
|
||||
|
||||
// If required tool call has already been called dont ask for it again
|
||||
boolean hasBeenExecuted = hasToolCallBeenExecuted(chatRequestMessages, toolChoiceName);
|
||||
if (hasBeenExecuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ChatCompletionsToolDefinition> toolDefinitions = new ArrayList<>();
|
||||
|
||||
toolDefinitions.add(new ChatCompletionsFunctionToolDefinition(
|
||||
OpenAIFunction.toFunctionDefinition(
|
||||
toolChoice.getMetadata(),
|
||||
toolChoice.getPluginName())));
|
||||
|
||||
options.setTools(toolDefinitions);
|
||||
try {
|
||||
String json = String.format(
|
||||
"{\"type\":\"function\",\"function\":{\"name\":\"%s\"}}", toolChoiceName);
|
||||
options.setToolChoice(BinaryData.fromObject(new ObjectMapper().readTree(json)));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If a set of functions are enabled to be called
|
||||
ToolCallBehavior.AllowedKernelFunctions enabledKernelFunctions = (ToolCallBehavior.AllowedKernelFunctions) toolCallBehavior;
|
||||
List<ChatCompletionsToolDefinition> 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());
|
||||
|
||||
if (toolDefinitions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.setTools(toolDefinitions);
|
||||
options.setToolChoice(BinaryData.fromString("auto"));
|
||||
}
|
||||
|
||||
private static boolean hasToolCallBeenExecuted(List<ChatRequestMessage> chatRequestMessages,
|
||||
String toolChoiceName) {
|
||||
return chatRequestMessages
|
||||
.stream()
|
||||
.flatMap(message -> {
|
||||
// Extract tool calls
|
||||
if (message instanceof ChatRequestAssistantMessage) {
|
||||
return ((ChatRequestAssistantMessage) message).getToolCalls().stream();
|
||||
}
|
||||
return Stream.empty();
|
||||
})
|
||||
.filter(toolCall -> {
|
||||
// Filter if tool call has correct name
|
||||
if (toolCall instanceof ChatCompletionsFunctionToolCall) {
|
||||
return ((ChatCompletionsFunctionToolCall) toolCall).getFunction().getName()
|
||||
.equals(toolChoiceName);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.allMatch(toolcall -> {
|
||||
String id = toolcall.getId();
|
||||
// True if tool call id has a response message
|
||||
return chatRequestMessages
|
||||
.stream()
|
||||
.filter(
|
||||
chatRequestMessage -> chatRequestMessage instanceof ChatRequestToolMessage)
|
||||
.anyMatch(
|
||||
chatRequestMessage -> ((ChatRequestToolMessage) chatRequestMessage)
|
||||
.getToolCallId()
|
||||
.equals(id));
|
||||
});
|
||||
}
|
||||
|
||||
private static List<ChatRequestMessage> getChatRequestMessages(ChatHistory chatHistory) {
|
||||
List<ChatMessageContent<?>> messages = chatHistory.getMessages();
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return messages.stream()
|
||||
.map(OpenAIChatCompletion::getChatRequestMessage)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static ChatRequestMessage getChatRequestMessage(
|
||||
ChatMessageContent<?> message) {
|
||||
|
||||
AuthorRole authorRole = message.getAuthorRole();
|
||||
String content = message.getContent();
|
||||
|
||||
switch (authorRole) {
|
||||
case ASSISTANT:
|
||||
return formAssistantMessage(message, content);
|
||||
case SYSTEM:
|
||||
return new ChatRequestSystemMessage(content);
|
||||
case USER:
|
||||
return new ChatRequestUserMessage(content);
|
||||
case TOOL:
|
||||
String id = null;
|
||||
|
||||
if (message.getMetadata() != null) {
|
||||
id = message.getMetadata().getId();
|
||||
}
|
||||
|
||||
if (id == null) {
|
||||
throw new SKException(
|
||||
"Require to create a tool call message, but not tool call id is available");
|
||||
}
|
||||
return new ChatRequestToolMessage(content, id);
|
||||
default:
|
||||
LOGGER.debug("Unexpected author role: {}", authorRole);
|
||||
throw new SKException("Unexpected author role: " + authorRole);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static ChatRequestAssistantMessage formAssistantMessage(
|
||||
ChatMessageContent<?> message,
|
||||
@Nullable String content) {
|
||||
// TODO: handle tools other than function calls
|
||||
ChatRequestAssistantMessage asstMessage = new ChatRequestAssistantMessage(content);
|
||||
|
||||
List<OpenAIFunctionToolCall> toolCalls = null;
|
||||
if (message instanceof OpenAIChatMessageContent) {
|
||||
toolCalls = ((OpenAIChatMessageContent<?>) message).getToolCall();
|
||||
}
|
||||
|
||||
if (toolCalls != null) {
|
||||
asstMessage.setToolCalls(
|
||||
toolCalls.stream()
|
||||
.map(toolCall -> {
|
||||
KernelFunctionArguments 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("{", "}", ","))
|
||||
: "{}";
|
||||
FunctionCall fnCall = new FunctionCall(toolCall.getFunctionName(),
|
||||
args);
|
||||
return new ChatCompletionsFunctionToolCall(toolCall.getId(),
|
||||
fnCall);
|
||||
})
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
return asstMessage;
|
||||
}
|
||||
|
||||
static ChatRequestMessage getChatRequestMessage(
|
||||
AuthorRole authorRole,
|
||||
String content) {
|
||||
|
||||
switch (authorRole) {
|
||||
case ASSISTANT:
|
||||
return new ChatRequestAssistantMessage(content);
|
||||
case SYSTEM:
|
||||
return new ChatRequestSystemMessage(content);
|
||||
case USER:
|
||||
return new ChatRequestUserMessage(content);
|
||||
case TOOL:
|
||||
return new ChatRequestToolMessage(content, null);
|
||||
default:
|
||||
LOGGER.debug("Unexpected author role: " + authorRole);
|
||||
throw new SKException("Unexpected author role: " + authorRole);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for creating a new instance of {@link OpenAIChatCompletion}.
|
||||
*/
|
||||
public static class Builder extends ChatCompletionService.Builder {
|
||||
|
||||
@Override
|
||||
public OpenAIChatCompletion build() {
|
||||
|
||||
if (this.client == null) {
|
||||
throw new AIException(AIException.ErrorCodes.INVALID_REQUEST,
|
||||
"OpenAI client must be provided");
|
||||
}
|
||||
|
||||
if (this.modelId == null || modelId.isEmpty()) {
|
||||
throw new AIException(AIException.ErrorCodes.INVALID_REQUEST,
|
||||
"OpenAI model id must be provided");
|
||||
}
|
||||
|
||||
return new OpenAIChatCompletion(client, modelId, serviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// 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.ChatMessageContent;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents the content of a chat message.
|
||||
*
|
||||
* @param <T> The type of the inner content.
|
||||
*/
|
||||
public class OpenAIChatMessageContent<T> extends ChatMessageContent<T> {
|
||||
|
||||
@Nullable
|
||||
private final List<OpenAIFunctionToolCall> toolCall;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@link OpenAIChatMessageContent} 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 toolCall The tool call.
|
||||
*/
|
||||
public OpenAIChatMessageContent(
|
||||
AuthorRole authorRole,
|
||||
String content,
|
||||
@Nullable String modelId,
|
||||
@Nullable T innerContent,
|
||||
@Nullable Charset encoding,
|
||||
@Nullable FunctionResultMetadata metadata,
|
||||
@Nullable List<OpenAIFunctionToolCall> toolCall) {
|
||||
super(authorRole, content, modelId, innerContent, encoding, metadata);
|
||||
|
||||
if (toolCall == null) {
|
||||
this.toolCall = null;
|
||||
} else {
|
||||
this.toolCall = Collections.unmodifiableList(toolCall);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets any tool calls requested.
|
||||
*
|
||||
* @return The tool call.
|
||||
*/
|
||||
@Nullable
|
||||
public List<OpenAIFunctionToolCall> getToolCall() {
|
||||
return toolCall;
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the response from the OpenAI chat completion API.
|
||||
*/
|
||||
public interface OpenAIChatResponse {
|
||||
|
||||
/**
|
||||
* Represents the usage of the chat completion API.
|
||||
*/
|
||||
interface Usage {
|
||||
|
||||
/**
|
||||
* Gets the number of tokens used for the prompt.
|
||||
*
|
||||
* @return the number of tokens used for the prompt
|
||||
*/
|
||||
@JsonProperty("prompt_tokens")
|
||||
int getPromptTokens();
|
||||
|
||||
/**
|
||||
* Gets the number of tokens used for the completion.
|
||||
*
|
||||
* @return the number of tokens used for the completion
|
||||
*/
|
||||
@JsonProperty("completion_tokens")
|
||||
int getCompletionTokens();
|
||||
|
||||
/**
|
||||
* Gets the total number of tokens used.
|
||||
*
|
||||
* @return the total number of tokens used
|
||||
*/
|
||||
@JsonProperty("total_tokens")
|
||||
int getTotalTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a choice in the chat completion response.
|
||||
*/
|
||||
interface Choice {
|
||||
|
||||
/**
|
||||
* Gets the message in the chat completion response.
|
||||
*
|
||||
* @return the message in the chat completion response
|
||||
*/
|
||||
@JsonProperty("message")
|
||||
Message getMessage();
|
||||
|
||||
/**
|
||||
* Gets the finish details in the chat completion response.
|
||||
*
|
||||
* @return the finish details in the chat completion response
|
||||
*/
|
||||
@JsonProperty("finish_details")
|
||||
FinishDetails getFinishDetails();
|
||||
|
||||
/**
|
||||
* Gets the index of the choice.
|
||||
*
|
||||
* @return the index of the choice
|
||||
*/
|
||||
@JsonProperty("index")
|
||||
int getIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a message in the chat completion response.
|
||||
*/
|
||||
interface Message {
|
||||
|
||||
/**
|
||||
* Gets the role of the message.
|
||||
*
|
||||
* @return the role of the message
|
||||
*/
|
||||
@JsonProperty("role")
|
||||
String getRole();
|
||||
|
||||
/**
|
||||
* Gets the content of the message.
|
||||
*
|
||||
* @return the content of the message
|
||||
*/
|
||||
@JsonProperty("content")
|
||||
String getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the finish details in the chat completion response.
|
||||
*/
|
||||
interface FinishDetails {
|
||||
|
||||
/**
|
||||
* Gets the type of the finish details.
|
||||
*
|
||||
* @return the type of the finish details
|
||||
*/
|
||||
@JsonProperty("type")
|
||||
String getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the id of the chat completion response.
|
||||
*
|
||||
* @return the id of the chat completion response
|
||||
*/
|
||||
@JsonProperty("id")
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Gets the object of the chat completion response.
|
||||
*
|
||||
* @return the object of the chat completion response
|
||||
*/
|
||||
@JsonProperty("object")
|
||||
String getObject();
|
||||
|
||||
/**
|
||||
* Gets the created time of the chat completion response.
|
||||
*
|
||||
* @return the created time of the chat completion response
|
||||
*/
|
||||
@JsonProperty("created")
|
||||
Long getCreated();
|
||||
|
||||
/**
|
||||
* Gets the model of the chat completion response.
|
||||
*
|
||||
* @return the model of the chat completion response
|
||||
*/
|
||||
@JsonProperty("model")
|
||||
String getModel();
|
||||
|
||||
/**
|
||||
* Gets the usage of the chat completion response.
|
||||
*
|
||||
* @return the usage of the chat completion response
|
||||
*/
|
||||
@JsonProperty("Usage")
|
||||
List<Usage> getUsage();
|
||||
|
||||
/**
|
||||
* Gets the choices of the chat completion response.
|
||||
*
|
||||
* @return the choices of the chat completion response
|
||||
*/
|
||||
@JsonProperty("choices")
|
||||
List<Choice> getChoices();
|
||||
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
|
||||
|
||||
import com.azure.ai.openai.models.FunctionDefinition;
|
||||
import com.azure.core.util.BinaryData;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
class OpenAIFunction {
|
||||
|
||||
private final String pluginName;
|
||||
private final String name;
|
||||
private final FunctionDefinition functionDefinition;
|
||||
|
||||
protected OpenAIFunction(
|
||||
@Nonnull String name,
|
||||
@Nonnull String pluginName,
|
||||
@Nonnull FunctionDefinition functionDefinition) {
|
||||
this.name = name;
|
||||
this.pluginName = pluginName;
|
||||
this.functionDefinition = functionDefinition;
|
||||
}
|
||||
|
||||
public static OpenAIFunction build(KernelFunctionMetadata<?> metadata, String pluginName) {
|
||||
String name = metadata.getName();
|
||||
FunctionDefinition functionDefinition = toFunctionDefinition(metadata, pluginName);
|
||||
return new OpenAIFunction(name, pluginName, functionDefinition);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPluginName() {
|
||||
return pluginName;
|
||||
}
|
||||
|
||||
public FunctionDefinition getFunctionDefinition() {
|
||||
return functionDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the separator used between the plugin name and the function name, if a plugin name is
|
||||
* present.
|
||||
*
|
||||
* @return The separator used between the plugin name and the function name.
|
||||
*/
|
||||
public static String getNameSeparator() {
|
||||
return "-";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fully-qualified name of the function.
|
||||
*
|
||||
* @return The fully-qualified name of the function.
|
||||
*/
|
||||
private static String getFullyQualifiedName(
|
||||
@Nullable String pluginName, String functionName) {
|
||||
return (pluginName == null || pluginName.isEmpty()) ? functionName
|
||||
: pluginName + getNameSeparator() + functionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a KernelFunctionMetadata representation to the SDK's FunctionDefinition
|
||||
* representation.
|
||||
*
|
||||
* @return A FunctionDefinition containing all the function information.
|
||||
*/
|
||||
public static FunctionDefinition toFunctionDefinition(KernelFunctionMetadata<?> metadata,
|
||||
@Nullable String pluginName) {
|
||||
BinaryData resultParameters;
|
||||
|
||||
Map<String, JsonNode> properties = new HashMap<>();
|
||||
List<String> required = new ArrayList<>();
|
||||
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
for (InputVariable parameter : metadata.getParameters()) {
|
||||
String parameterJsonSchema = getSchemaForFunctionParameter(parameter);
|
||||
|
||||
properties.put(parameter.getName(), objectMapper.readTree(parameterJsonSchema));
|
||||
|
||||
if (parameter.isRequired()) {
|
||||
required.add(parameter.getName());
|
||||
}
|
||||
}
|
||||
|
||||
String json = objectMapper
|
||||
.writeValueAsString(new OpenAIFunctionParameter("object", required, properties));
|
||||
resultParameters = BinaryData.fromObject(objectMapper.readTree(json));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
FunctionDefinition functionDefinition = new FunctionDefinition(
|
||||
getFullyQualifiedName(pluginName, metadata.getName()));
|
||||
functionDefinition.setDescription(metadata.getDescription());
|
||||
functionDefinition.setParameters(resultParameters);
|
||||
|
||||
return functionDefinition;
|
||||
}
|
||||
|
||||
private static class OpenAIFunctionParameter {
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
@JsonProperty("required")
|
||||
private List<String> required;
|
||||
@JsonProperty("properties")
|
||||
private Map<String, JsonNode> properties;
|
||||
|
||||
public OpenAIFunctionParameter(
|
||||
String type,
|
||||
List<String> required,
|
||||
Map<String, JsonNode> properties) {
|
||||
this.type = type;
|
||||
this.required = Collections.unmodifiableList(required);
|
||||
this.properties = Collections.unmodifiableMap(properties);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
public List<String> getRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
public Map<String, JsonNode> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getSchemaForFunctionParameter(@Nullable InputVariable parameter) {
|
||||
List<String> entries = new ArrayList<>();
|
||||
|
||||
entries.add("\"type\":\"string\"");
|
||||
|
||||
// Add description if present
|
||||
if (parameter != null && parameter.getDescription() != null && !parameter.getDescription()
|
||||
.isEmpty()) {
|
||||
String description = parameter.getDescription();
|
||||
description = description.replace("\n", "");
|
||||
description = description.replace("\"", "\\\"");
|
||||
|
||||
description = String.format("\"description\":\"%s\"", description);
|
||||
entries.add(description);
|
||||
}
|
||||
|
||||
// Add enum options if parameter is an enum
|
||||
if (parameter != null && parameter.getEnumValues() != null && !parameter.getEnumValues()
|
||||
.isEmpty()) {
|
||||
String enumEntry = parameter
|
||||
.getEnumValues()
|
||||
.stream()
|
||||
.map(Object::toString)
|
||||
.map(it -> "\"" + it + "\"")
|
||||
.collect(Collectors.joining(","));
|
||||
|
||||
entries.add("\"enum\":[ " + enumEntry + " ]");
|
||||
}
|
||||
|
||||
String schema = String.join(",", entries);
|
||||
|
||||
return "{" + schema + "}";
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
|
||||
|
||||
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents a call to a function in the OpenAI tool.
|
||||
*/
|
||||
public class OpenAIFunctionToolCall {
|
||||
|
||||
/// <summary>Gets the ID of the tool call.</summary>
|
||||
@Nullable
|
||||
private final String id;
|
||||
|
||||
/// <summary>Gets the name of the plugin with which this function is associated, if any.</summary>
|
||||
|
||||
@Nullable
|
||||
private final String pluginName;
|
||||
|
||||
/// <summary>Gets the name of the function.</summary>
|
||||
private final String functionName;
|
||||
|
||||
/// <summary>Gets a name/value collection of the arguments to the function, if any.</summary>
|
||||
@Nullable
|
||||
private final KernelFunctionArguments arguments;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@link OpenAIFunctionToolCall} class.
|
||||
*
|
||||
* @param id The ID of the tool call.
|
||||
* @param pluginName The name of the plugin with which this function is associated, if any.
|
||||
* @param functionName The name of the function.
|
||||
* @param arguments A name/value collection of the arguments to the function, if any.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
|
||||
|
||||
import com.azure.ai.openai.models.ChatRequestMessage;
|
||||
import com.azure.ai.openai.models.FunctionDefinition;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
class ParsedPrompt {
|
||||
|
||||
private final List<ChatRequestMessage> chatRequestMessages;
|
||||
private final List<FunctionDefinition> functions;
|
||||
|
||||
protected ParsedPrompt(List<ChatRequestMessage> parsedMessages,
|
||||
@Nullable List<FunctionDefinition> parsedFunctions) {
|
||||
this.chatRequestMessages = parsedMessages;
|
||||
if (parsedFunctions == null) {
|
||||
parsedFunctions = new ArrayList<>();
|
||||
}
|
||||
this.functions = parsedFunctions;
|
||||
}
|
||||
|
||||
public List<ChatRequestMessage> getChatRequestMessages() {
|
||||
return chatRequestMessages;
|
||||
}
|
||||
|
||||
public List<FunctionDefinition> getFunctions() {
|
||||
return functions;
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
|
||||
|
||||
import com.azure.ai.openai.models.ChatRequestAssistantMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestFunctionMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestSystemMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestToolMessage;
|
||||
import com.azure.ai.openai.models.ChatRequestUserMessage;
|
||||
import com.azure.ai.openai.models.FunctionDefinition;
|
||||
import com.azure.core.util.BinaryData;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.semantickernel.exceptions.SKException;
|
||||
import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
|
||||
import com.microsoft.semantickernel.services.chatcompletion.AuthorRole;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.xml.namespace.QName;
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
import javax.xml.stream.XMLInputFactory;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.stream.events.Attribute;
|
||||
import javax.xml.stream.events.StartElement;
|
||||
import javax.xml.stream.events.XMLEvent;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class XMLPromptParser {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(XMLPromptParser.class);
|
||||
|
||||
public static ParsedPrompt parse(String rawPrompt) {
|
||||
List<String> prompts = Arrays.asList(
|
||||
rawPrompt,
|
||||
"<prompt>" + rawPrompt + "</prompt>");
|
||||
|
||||
for (String prompt : prompts) {
|
||||
try {
|
||||
List<ChatRequestMessage> parsedMessages = getChatRequestMessages(prompt);
|
||||
List<FunctionDefinition> parsedFunctions = getFunctionDefinitions(prompt);
|
||||
|
||||
if (!parsedMessages.isEmpty()) {
|
||||
return new ParsedPrompt(parsedMessages, parsedFunctions);
|
||||
}
|
||||
} catch (SKException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
ChatRequestUserMessage message = new ChatRequestUserMessage(rawPrompt);
|
||||
|
||||
if (message.getName() == null) {
|
||||
message.setName(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
return new ParsedPrompt(Collections.singletonList(message), null);
|
||||
}
|
||||
|
||||
private static List<ChatRequestMessage> getChatRequestMessages(String prompt) {
|
||||
|
||||
// TODO: XML parsing should be done as a chain of XMLEvent handlers.
|
||||
// If one handler does not recognize the element, it should pass it to the next handler.
|
||||
// In this way, we can avoid parsing the whole prompt twice and easily extend the parsing logic.
|
||||
List<ChatRequestMessage> messages = new ArrayList<>();
|
||||
try (InputStream is = new ByteArrayInputStream(prompt.getBytes(StandardCharsets.UTF_8))) {
|
||||
XMLInputFactory factory = XMLInputFactory.newInstance();
|
||||
XMLEventReader reader = factory.createXMLEventReader(is);
|
||||
while (reader.hasNext()) {
|
||||
XMLEvent event = reader.nextEvent();
|
||||
if (event.isStartElement()) {
|
||||
String name = getElementName(event);
|
||||
if (name.equals("message")) {
|
||||
String role = getAttributeValue(event, "role");
|
||||
String content = reader.getElementText();
|
||||
messages.add(getChatRequestMessage(role, content));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException | XMLStreamException | IllegalArgumentException e) {
|
||||
throw new SKException("Failed to parse messages");
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private static List<FunctionDefinition> getFunctionDefinitions(String prompt) {
|
||||
// TODO: XML parsing should be done as a chain of XMLEvent handlers. See previous remark.
|
||||
// <function pluginName=\"%s\" name=\"%s\" description=\"%s\">
|
||||
// <parameter name=\"%s\" description=\"%s\" defaultValue=\"%s\" isRequired=\"%s\" type=\"%s\"/>...
|
||||
// </function>
|
||||
List<FunctionDefinition> functionDefinitions = new ArrayList<>();
|
||||
try (InputStream is = new ByteArrayInputStream(prompt.getBytes(StandardCharsets.UTF_8))) {
|
||||
XMLInputFactory factory = XMLInputFactory.newFactory();
|
||||
XMLEventReader reader = factory.createXMLEventReader(is);
|
||||
FunctionDefinition functionDefinition = null;
|
||||
Map<String, String> parameters = new HashMap<>();
|
||||
List<String> requiredParmeters = new ArrayList<>();
|
||||
while (reader.hasNext()) {
|
||||
XMLEvent event = reader.nextEvent();
|
||||
if (event.isStartElement()) {
|
||||
String elementName = getElementName(event);
|
||||
if (elementName.equals("function")) {
|
||||
assert functionDefinition == null;
|
||||
assert parameters.isEmpty();
|
||||
assert requiredParmeters.isEmpty();
|
||||
String pluginName = getAttributeValue(event, "pluginName");
|
||||
String name = getAttributeValue(event, "name");
|
||||
String description = getAttributeValue(event, "description");
|
||||
// name has to match '^[a-zA-Z0-9_-]{1,64}$'
|
||||
functionDefinition = new FunctionDefinition(
|
||||
ToolCallBehavior.formFullFunctionName(pluginName, name))
|
||||
.setDescription(description);
|
||||
} else if (elementName.equals("parameter")) {
|
||||
String name = getAttributeValue(event, "name");
|
||||
String type = getAttributeValue(event, "type").toLowerCase(Locale.ROOT);
|
||||
String description = getAttributeValue(event, "description");
|
||||
parameters.put(name,
|
||||
String.format("{\"type\": \"%s\", \"description\": \"%s\"}",
|
||||
"string",
|
||||
description));
|
||||
|
||||
String isRequired = getAttributeValue(event, "isRequired");
|
||||
if (Boolean.parseBoolean(isRequired)) {
|
||||
requiredParmeters.add(name);
|
||||
}
|
||||
}
|
||||
} else if (event.isEndElement()) {
|
||||
String elementName = getElementName(event);
|
||||
if (elementName.equals("function")) {
|
||||
// Example JSON Schema:
|
||||
// {
|
||||
// "type": "function",
|
||||
// "function": {
|
||||
// "name": "get_current_weather",
|
||||
// "description": "Get the current weather in a given location",
|
||||
// "parameters": {
|
||||
// "type": "object",
|
||||
// "properties": {
|
||||
// "location": {
|
||||
// "type": "string",
|
||||
// "description": "The city and state, e.g. San Francisco, CA",
|
||||
// },
|
||||
// "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
|
||||
// },
|
||||
// "required": ["location"],
|
||||
// },
|
||||
// },
|
||||
//}
|
||||
if (functionDefinition == null) {
|
||||
throw new SKException("Failed to parse function definition");
|
||||
}
|
||||
if (!parameters.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder(
|
||||
"{\"type\": \"object\", \"properties\": {");
|
||||
parameters.forEach((name, value) -> {
|
||||
// make "param": {"type": "string", "description": "desc"},
|
||||
sb.append(String.format("\"%s\": %s,", name, value));
|
||||
});
|
||||
// strip off trailing comma and close the properties object
|
||||
sb.replace(sb.length() - 1, sb.length(), "}");
|
||||
if (!requiredParmeters.isEmpty()) {
|
||||
sb.append(", \"required\": [");
|
||||
requiredParmeters.forEach(name -> {
|
||||
sb.append(String.format("\"%s\",", name));
|
||||
});
|
||||
// strip off trailing comma and close the required array
|
||||
sb.replace(sb.length() - 1, sb.length(), "]");
|
||||
}
|
||||
// close the object
|
||||
sb.append("}");
|
||||
//System.out.println(sb.toString());
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonNode jsonNode = objectMapper.readTree(sb.toString());
|
||||
BinaryData binaryData = BinaryData.fromObject(jsonNode);
|
||||
functionDefinition.setParameters(binaryData);
|
||||
}
|
||||
functionDefinitions.add(functionDefinition);
|
||||
functionDefinition = null;
|
||||
parameters.clear();
|
||||
requiredParmeters.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException | XMLStreamException | IllegalArgumentException e) {
|
||||
LOGGER.error("Error parsing prompt", e);
|
||||
}
|
||||
return functionDefinitions;
|
||||
}
|
||||
|
||||
private static String getElementName(XMLEvent xmlEvent) {
|
||||
if (xmlEvent.isStartElement()) {
|
||||
return xmlEvent.asStartElement().getName().getLocalPart();
|
||||
} else if (xmlEvent.isEndElement()) {
|
||||
return xmlEvent.asEndElement().getName().getLocalPart();
|
||||
}
|
||||
// TODO: programmer's error - log at debug
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String getAttributeValue(XMLEvent xmlEvent, String attributeName) {
|
||||
if (xmlEvent.isStartElement()) {
|
||||
StartElement element = xmlEvent.asStartElement();
|
||||
Attribute attribute = element.getAttributeByName(QName.valueOf(attributeName));
|
||||
return attribute != null ? attribute.getValue() : "";
|
||||
}
|
||||
// TODO: programmer's error - log at debug
|
||||
return "";
|
||||
}
|
||||
|
||||
private static ChatRequestMessage getChatRequestMessage(
|
||||
String role,
|
||||
String content) {
|
||||
try {
|
||||
AuthorRole authorRole = AuthorRole.valueOf(role.toUpperCase(Locale.ROOT));
|
||||
return OpenAIChatCompletion.getChatRequestMessage(authorRole, content);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.debug("Unknown author role: " + role);
|
||||
throw new SKException("Unknown author role: " + role);
|
||||
}
|
||||
}
|
||||
|
||||
public static ChatRequestMessage unescapeRequest(ChatRequestMessage message) {
|
||||
if (message instanceof ChatRequestUserMessage) {
|
||||
ChatRequestUserMessage chatRequestMessage = (ChatRequestUserMessage) message;
|
||||
String content = StringEscapeUtils.unescapeXml(
|
||||
chatRequestMessage.getContent().toString());
|
||||
|
||||
return new ChatRequestUserMessage(content)
|
||||
.setName(chatRequestMessage.getName());
|
||||
} else if (message instanceof ChatRequestSystemMessage) {
|
||||
ChatRequestSystemMessage chatRequestMessage = (ChatRequestSystemMessage) message;
|
||||
String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent());
|
||||
|
||||
return new ChatRequestSystemMessage(content)
|
||||
.setName(chatRequestMessage.getName());
|
||||
} else if (message instanceof ChatRequestAssistantMessage) {
|
||||
ChatRequestAssistantMessage chatRequestMessage = (ChatRequestAssistantMessage) message;
|
||||
String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent());
|
||||
|
||||
return new ChatRequestAssistantMessage(content)
|
||||
.setToolCalls(chatRequestMessage.getToolCalls())
|
||||
.setFunctionCall(chatRequestMessage.getFunctionCall())
|
||||
.setName(chatRequestMessage.getName());
|
||||
} else if (message instanceof ChatRequestFunctionMessage) {
|
||||
ChatRequestFunctionMessage chatRequestMessage = (ChatRequestFunctionMessage) message;
|
||||
String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent());
|
||||
|
||||
return new ChatRequestFunctionMessage(
|
||||
chatRequestMessage.getName(),
|
||||
content);
|
||||
} else if (message instanceof ChatRequestToolMessage) {
|
||||
ChatRequestToolMessage chatRequestMessage = (ChatRequestToolMessage) message;
|
||||
String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent());
|
||||
|
||||
return new ChatRequestToolMessage(
|
||||
content,
|
||||
chatRequestMessage.getToolCallId());
|
||||
}
|
||||
|
||||
throw new SKException("Unknown message type: " + message.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.implementation;
|
||||
|
||||
import com.azure.core.http.HttpHeaderName;
|
||||
import com.azure.core.http.rest.RequestOptions;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/**
|
||||
* Provides Http request settings for OpenAI requests.
|
||||
*/
|
||||
public final class OpenAIRequestSettings {
|
||||
|
||||
private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(
|
||||
OpenAIRequestSettings.class);
|
||||
|
||||
private static final String SEMANTIC_KERNEL_VERSION_PROPERTY_NAME = "semantic-kernel.version";
|
||||
private static final String SEMANTIC_KERNEL_VERSION_PROPERTIES_FILE = "semantic-kernel-version.properties";
|
||||
private static final String version;
|
||||
|
||||
static {
|
||||
version = loadVersion();
|
||||
}
|
||||
|
||||
private static String loadVersion() {
|
||||
|
||||
String version = "Java/unknown";
|
||||
|
||||
try (InputStream settingsFile = OpenAIRequestSettings.class.getResourceAsStream(
|
||||
SEMANTIC_KERNEL_VERSION_PROPERTIES_FILE)) {
|
||||
|
||||
Properties props = new Properties();
|
||||
props.load(settingsFile);
|
||||
if (props.containsKey(SEMANTIC_KERNEL_VERSION_PROPERTY_NAME)) {
|
||||
String skVersion = props.getProperty(SEMANTIC_KERNEL_VERSION_PROPERTY_NAME);
|
||||
if (skVersion != null && !skVersion.isEmpty()) {
|
||||
return "Java/" + skVersion;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
//Ignore
|
||||
LOGGER.trace("Failed to load Semantic Kernel version from properties file", e);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTTP request options for OpenAI requests.
|
||||
*
|
||||
* @return The request options
|
||||
*/
|
||||
public static RequestOptions getRequestOptions() {
|
||||
return new RequestOptions()
|
||||
.setHeader(HttpHeaderName.fromString("Semantic-Kernel-Version"), version);
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.aiservices.openai.textcompletion;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.models.CompletionsOptions;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.aiservices.openai.OpenAiService;
|
||||
import com.microsoft.semantickernel.aiservices.openai.implementation.OpenAIRequestSettings;
|
||||
import com.microsoft.semantickernel.exceptions.AIException;
|
||||
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.textcompletion.TextContent;
|
||||
import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* An OpenAI implementation of a {@link TextGenerationService}.
|
||||
*/
|
||||
public class OpenAITextGenerationService extends OpenAiService implements TextGenerationService {
|
||||
|
||||
/**
|
||||
* Creates a new {@link OpenAITextGenerationService}.
|
||||
*
|
||||
* @param client OpenAI client
|
||||
* @param modelId OpenAI model id
|
||||
* @param serviceId Service id
|
||||
*/
|
||||
protected OpenAITextGenerationService(
|
||||
OpenAIAsyncClient client,
|
||||
String modelId,
|
||||
@Nullable String serviceId) {
|
||||
super(client, serviceId, modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for creating a {@link OpenAITextGenerationService}.
|
||||
*
|
||||
* @return A new {@link OpenAITextGenerationService} builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<TextContent>> getTextContentsAsync(
|
||||
String prompt,
|
||||
@Nullable PromptExecutionSettings executionSettings,
|
||||
@Nullable Kernel kernel) {
|
||||
return this.internalCompleteTextAsync(prompt, executionSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<StreamingTextContent> getStreamingTextContentsAsync(
|
||||
String prompt,
|
||||
@Nullable PromptExecutionSettings executionSettings,
|
||||
@Nullable Kernel kernel) {
|
||||
return this
|
||||
.internalCompleteTextAsync(prompt, executionSettings)
|
||||
.flatMapMany(it -> Flux.fromStream(it.stream())
|
||||
.map(StreamingTextContent::new));
|
||||
}
|
||||
|
||||
protected Mono<List<TextContent>> internalCompleteTextAsync(
|
||||
String text,
|
||||
@Nullable PromptExecutionSettings requestSettings) {
|
||||
|
||||
CompletionsOptions completionsOptions = getCompletionsOptions(text, requestSettings);
|
||||
|
||||
return getClient()
|
||||
.getCompletionsWithResponse(getModelId(), completionsOptions,
|
||||
OpenAIRequestSettings.getRequestOptions())
|
||||
.flatMap(completionsResult -> {
|
||||
if (completionsResult.getStatusCode() >= 400) {
|
||||
return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR,
|
||||
"Request failed: " + completionsResult.getStatusCode()));
|
||||
}
|
||||
return Mono.just(completionsResult.getValue());
|
||||
})
|
||||
.map(completions -> {
|
||||
FunctionResultMetadata metadata = FunctionResultMetadata.build(
|
||||
completions.getId(),
|
||||
completions.getUsage(),
|
||||
completions.getCreatedAt());
|
||||
|
||||
return completions
|
||||
.getChoices()
|
||||
.stream()
|
||||
.map(choice -> {
|
||||
return new TextContent(
|
||||
choice.getText(),
|
||||
completionsOptions.getModel(),
|
||||
metadata);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
private CompletionsOptions getCompletionsOptions(
|
||||
String text,
|
||||
@Nullable PromptExecutionSettings requestSettings) {
|
||||
text = StringEscapeUtils.unescapeXml(text);
|
||||
|
||||
if (requestSettings == null) {
|
||||
return new CompletionsOptions(Collections.singletonList(text))
|
||||
.setMaxTokens(PromptExecutionSettings.DEFAULT_MAX_TOKENS);
|
||||
}
|
||||
if (requestSettings.getMaxTokens() < 1) {
|
||||
throw new AIException(AIException.ErrorCodes.INVALID_REQUEST, "Max tokens must be >0");
|
||||
}
|
||||
if (requestSettings.getResultsPerPrompt() < 1
|
||||
|| requestSettings.getResultsPerPrompt() > MAX_RESULTS_PER_PROMPT) {
|
||||
throw new AIException(AIException.ErrorCodes.INVALID_REQUEST,
|
||||
String.format("Results per prompt must be in range between 1 and %d, inclusive.",
|
||||
MAX_RESULTS_PER_PROMPT));
|
||||
}
|
||||
|
||||
CompletionsOptions options = new CompletionsOptions(Collections.singletonList(text))
|
||||
.setMaxTokens(requestSettings.getMaxTokens())
|
||||
.setTemperature(requestSettings.getTemperature())
|
||||
.setTopP(requestSettings.getTopP())
|
||||
.setFrequencyPenalty(requestSettings.getFrequencyPenalty())
|
||||
.setPresencePenalty(requestSettings.getPresencePenalty())
|
||||
.setModel(getModelId())
|
||||
.setN(requestSettings.getResultsPerPrompt())
|
||||
.setUser(requestSettings.getUser())
|
||||
.setBestOf(requestSettings.getBestOf())
|
||||
.setLogitBias(new HashMap<>());
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for a TextGenerationService
|
||||
*/
|
||||
public static class Builder extends TextGenerationService.Builder {
|
||||
|
||||
@Override
|
||||
public TextGenerationService build() {
|
||||
|
||||
if (this.client == null) {
|
||||
throw new AIException(AIException.ErrorCodes.INVALID_REQUEST,
|
||||
"OpenAI client must be provided");
|
||||
}
|
||||
if (this.modelId == null) {
|
||||
throw new AIException(AIException.ErrorCodes.INVALID_REQUEST,
|
||||
"OpenAI model id must be provided");
|
||||
}
|
||||
|
||||
return new OpenAITextGenerationService(
|
||||
this.client,
|
||||
this.modelId,
|
||||
this.serviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.microsoft.semantickernel.aiservices.openai.audio.OpenAiAudioToTextService$Builder
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.microsoft.semantickernel.aiservices.openai.audio.OpenAiTextToAudioService$Builder
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion$Builder
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.microsoft.semantickernel.aiservices.openai.textcompletion.OpenAITextGenerationService$Builder
|
||||
+1
@@ -0,0 +1 @@
|
||||
semantic-kernel.version=${project.version}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel.test</groupId>
|
||||
<artifactId>api-test</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>api-usage-example</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j2-impl</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,142 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.core.credential.AzureKeyCredential;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import com.microsoft.semantickernel.textcompletion.TextCompletion;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class InlineFunctionExample {
|
||||
public static final String AZURE_CONF_PROPERTIES = "conf.properties";
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(InlineFunctionExample.class);
|
||||
private static final String MODEL = "text-davinci-003";
|
||||
|
||||
private static final String API_KEY;
|
||||
private static final String ENDPOINT;
|
||||
private static final String TEXT_TO_SUMMARIZE =
|
||||
"""
|
||||
Demo (ancient Greek poet)
|
||||
From Wikipedia, the free encyclopedia
|
||||
Demo or Damo (Greek: Δεμώ, Δαμώ; fl. c. AD 200) was a Greek woman of the
|
||||
Roman period, known for a single epigram, engraved upon the Colossus of
|
||||
Memnon, which bears her name. She speaks of herself therein as a lyric
|
||||
poetess dedicated to the Muses, but nothing is known of her life.[1]
|
||||
Identity
|
||||
Demo was evidently Greek, as her name, a traditional epithet of Demeter,
|
||||
signifies. The name was relatively common in the Hellenistic world, in
|
||||
Egypt and elsewhere, and she cannot be further identified. The date of her
|
||||
visit to the Colossus of Memnon cannot be established with certainty, but
|
||||
internal evidence on the left leg suggests her poem was inscribed there at
|
||||
some point in or after AD 196.[2]
|
||||
Epigram
|
||||
There are a number of graffiti inscriptions on the Colossus of Memnon.
|
||||
Following three epigrams by Julia Balbilla, a fourth epigram, in elegiac
|
||||
couplets, entitled and presumably authored by "Demo" or "Damo" (the
|
||||
Greek inscription is difficult to read), is a dedication to the Muses.[2]
|
||||
The poem is traditionally published with the works of Balbilla, though the
|
||||
internal evidence suggests a different author.[1]
|
||||
In the poem, Demo explains that Memnon has shown her special respect. In
|
||||
return, Demo offers the gift for poetry, as a gift to the hero. At the end
|
||||
of this epigram, she addresses Memnon, highlighting his divine status by
|
||||
recalling his strength and holiness.[2]
|
||||
Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic
|
||||
dialect. The language indicates she was knowledgeable in Homeric
|
||||
poetry—'bearing a pleasant gift', for example, alludes to the use of that
|
||||
phrase throughout the Iliad and Odyssey.[a][2];
|
||||
""";
|
||||
|
||||
static {
|
||||
try {
|
||||
API_KEY = getToken();
|
||||
ENDPOINT = getEndpoint();
|
||||
} catch (IOException e) {
|
||||
throw new ExceptionInInitializerError(
|
||||
"Error reading config file or properties. " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String getToken() throws IOException {
|
||||
return getConfigValue("token");
|
||||
}
|
||||
|
||||
private static String getEndpoint() throws IOException {
|
||||
return getConfigValue("endpoint");
|
||||
}
|
||||
|
||||
private static String getConfigValue(String propertyName) throws IOException {
|
||||
String propertyValue;
|
||||
Path configPath = Paths.get(System.getProperty("user.home"), ".oai", AZURE_CONF_PROPERTIES);
|
||||
Properties props = new Properties();
|
||||
try (var reader = Files.newBufferedReader(configPath)) {
|
||||
props.load(reader);
|
||||
propertyValue = props.getProperty(propertyName);
|
||||
if (propertyValue == null || propertyValue.isEmpty()) {
|
||||
throw new IOException("No property for: " + propertyName + " in " + configPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Please create a file at " + configPath, e);
|
||||
}
|
||||
return propertyValue;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
OpenAIAsyncClient client =
|
||||
new OpenAIClientBuilder()
|
||||
.endpoint(ENDPOINT)
|
||||
.credential(new AzureKeyCredential(API_KEY))
|
||||
.buildAsyncClient();
|
||||
|
||||
TextCompletion textCompletion =
|
||||
SKBuilders.textCompletion().withOpenAIClient(client).withModelId(MODEL).build();
|
||||
String prompt = "{{$input}}\n" + "Summarize the content above.";
|
||||
|
||||
Kernel kernel = SKBuilders.kernel().withDefaultAIService(textCompletion).build();
|
||||
|
||||
CompletionSKFunction summarize =
|
||||
kernel.getSemanticFunctionBuilder()
|
||||
.withPromptTemplate(prompt)
|
||||
.withFunctionName("summarize")
|
||||
.withRequestSettings(
|
||||
SKBuilders.completionRequestSettings()
|
||||
.temperature(0.2)
|
||||
.topP(0.5)
|
||||
.maxTokens(2000)
|
||||
.presencePenalty(0)
|
||||
.frequencyPenalty(0)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
if (summarize == null) {
|
||||
LOGGER.error("Null function");
|
||||
return;
|
||||
}
|
||||
|
||||
CountDownLatch cdl = new CountDownLatch(1);
|
||||
summarize
|
||||
.invokeAsync(TEXT_TO_SUMMARIZE)
|
||||
.subscribe(
|
||||
context -> LOGGER.info("Result: {} ", context.getResult()),
|
||||
error -> {
|
||||
LOGGER.error("Error: {} ", error.getMessage());
|
||||
cdl.countDown();
|
||||
},
|
||||
() -> {
|
||||
LOGGER.info("Completed");
|
||||
cdl.countDown();
|
||||
});
|
||||
try {
|
||||
cdl.await();
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.error("Error: {} ", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.core.credential.AzureKeyCredential;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import com.microsoft.semantickernel.textcompletion.TextCompletion;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class InlineFunctionWithPreBuiltSkillExample {
|
||||
|
||||
public static final String AZURE_CONF_PROPERTIES = "conf.properties";
|
||||
private static final Logger LOGGER =
|
||||
LoggerFactory.getLogger(InlineFunctionWithPreBuiltSkillExample.class);
|
||||
private static final String MODEL = "text-davinci-003";
|
||||
|
||||
private static final String API_KEY;
|
||||
private static final String ENDPOINT;
|
||||
private static final String TEXT_TO_SUMMARIZE =
|
||||
"""
|
||||
Demo (ancient Greek poet)
|
||||
From Wikipedia, the free encyclopedia
|
||||
Demo or Damo (Greek: Δεμώ, Δαμώ; fl. c. AD 200) was a Greek woman of the
|
||||
Roman period, known for a single epigram, engraved upon the Colossus of
|
||||
Memnon, which bears her name. She speaks of herself therein as a lyric
|
||||
poetess dedicated to the Muses, but nothing is known of her life.[1]
|
||||
Identity
|
||||
Demo was evidently Greek, as her name, a traditional epithet of Demeter,
|
||||
signifies. The name was relatively common in the Hellenistic world, in
|
||||
Egypt and elsewhere, and she cannot be further identified. The date of her
|
||||
visit to the Colossus of Memnon cannot be established with certainty, but
|
||||
internal evidence on the left leg suggests her poem was inscribed there at
|
||||
some point in or after AD 196.[2]
|
||||
Epigram
|
||||
There are a number of graffiti inscriptions on the Colossus of Memnon.
|
||||
Following three epigrams by Julia Balbilla, a fourth epigram, in elegiac
|
||||
couplets, entitled and presumably authored by "Demo" or "Damo" (the
|
||||
Greek inscription is difficult to read), is a dedication to the Muses.[2]
|
||||
The poem is traditionally published with the works of Balbilla, though the
|
||||
internal evidence suggests a different author.[1]
|
||||
In the poem, Demo explains that Memnon has shown her special respect. In
|
||||
return, Demo offers the gift for poetry, as a gift to the hero. At the end
|
||||
of this epigram, she addresses Memnon, highlighting his divine status by
|
||||
recalling his strength and holiness.[2]
|
||||
Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic
|
||||
dialect. The language indicates she was knowledgeable in Homeric
|
||||
poetry—'bearing a pleasant gift', for example, alludes to the use of that
|
||||
phrase throughout the Iliad and Odyssey.[a][2];
|
||||
""";
|
||||
|
||||
static {
|
||||
try {
|
||||
API_KEY = getToken();
|
||||
ENDPOINT = getEndpoint();
|
||||
} catch (IOException e) {
|
||||
throw new ExceptionInInitializerError(
|
||||
"Error reading config file or properties. " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static String getToken() throws IOException {
|
||||
return getConfigValue("token");
|
||||
}
|
||||
|
||||
public static String getEndpoint() throws IOException {
|
||||
return getConfigValue("endpoint");
|
||||
}
|
||||
|
||||
private static String getConfigValue(String propertyName) throws IOException {
|
||||
String propertyValue;
|
||||
Path configPath = Paths.get(System.getProperty("user.home"), ".oai", AZURE_CONF_PROPERTIES);
|
||||
Properties props = new Properties();
|
||||
try (var reader = Files.newBufferedReader(configPath)) {
|
||||
props.load(reader);
|
||||
propertyValue = props.getProperty(propertyName);
|
||||
if (propertyValue == null || propertyValue.isEmpty()) {
|
||||
throw new IOException("No property for: " + propertyName + " in " + configPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Please create a file at " + configPath, e);
|
||||
}
|
||||
return propertyValue;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
OpenAIAsyncClient client =
|
||||
new OpenAIClientBuilder()
|
||||
.endpoint(ENDPOINT)
|
||||
.credential(new AzureKeyCredential(API_KEY))
|
||||
.buildAsyncClient();
|
||||
|
||||
TextCompletion textCompletion =
|
||||
SKBuilders.textCompletion().withOpenAIClient(client).withModelId(MODEL).build();
|
||||
String prompt = "{{$input}}\nSummarize the content above.";
|
||||
|
||||
Kernel kernel = SKBuilders.kernel().withDefaultAIService(textCompletion).build();
|
||||
|
||||
CompletionSKFunction summarize =
|
||||
SKBuilders.completionFunctions()
|
||||
.withKernel(kernel)
|
||||
.withPromptTemplate(prompt)
|
||||
.withFunctionName("summarize")
|
||||
.withRequestSettings(
|
||||
SKBuilders.completionRequestSettings()
|
||||
.temperature(0.2)
|
||||
.topP(0.5)
|
||||
.maxTokens(2000)
|
||||
.presencePenalty(0)
|
||||
.frequencyPenalty(0)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
if (summarize == null) {
|
||||
LOGGER.error("Null function");
|
||||
return;
|
||||
}
|
||||
|
||||
CountDownLatch cdl = new CountDownLatch(1);
|
||||
summarize
|
||||
.invokeAsync(TEXT_TO_SUMMARIZE)
|
||||
.subscribe(
|
||||
context -> LOGGER.info("Result: {} ", context.getResult()),
|
||||
error -> {
|
||||
LOGGER.error("Error: {} ", error.getMessage());
|
||||
cdl.countDown();
|
||||
},
|
||||
() -> {
|
||||
LOGGER.info("Completed");
|
||||
cdl.countDown();
|
||||
});
|
||||
try {
|
||||
cdl.await();
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.error("Error: {} ", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,21 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel.test</groupId>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>api-test</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<version>1.0.3-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Semantic Kernel Integration Tests</name>
|
||||
<artifactId>integration-tests</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-core</artifactId>
|
||||
<artifactId>semantickernel-sample-code</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -51,56 +53,14 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-ai-openai</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-planners</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-plugin-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>sample-code</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-jdbc</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-postgresql</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-mysql</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-sqlite</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.44.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
@@ -124,6 +84,13 @@
|
||||
<artifactId>mysql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.wiremock</groupId>
|
||||
<artifactId>wiremock</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -137,4 +104,84 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<id>recordmappings</id>
|
||||
<configuration>
|
||||
<executable>java</executable>
|
||||
<arguments>
|
||||
<argument>-Djavax.net.ssl.trustStore=scripts/client.truststore</argument>
|
||||
<argument>-Djavax.net.ssl.trustStorePassword=password</argument>
|
||||
</arguments>
|
||||
<mainClass>com.microsoft.semantickernel.syntaxexamples.WiremockRecord</mainClass>
|
||||
<classpathScope>test</classpathScope>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>run-wiremocks</id>
|
||||
<activation>
|
||||
<activeByDefault>false</activeByDefault>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>./generateCert.sh</executable>
|
||||
<workingDirectory>scripts</workingDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>**/WiremockExamplesIT.java</include>
|
||||
</includes>
|
||||
<reuseForks>false</reuseForks>
|
||||
<forkCount>1</forkCount>
|
||||
<argLine>
|
||||
-Djavax.net.ssl.trustStore=scripts/client.truststore
|
||||
-Djavax.net.ssl.trustStorePassword=password
|
||||
</argLine>
|
||||
<environmentVariables>
|
||||
<AZURE_CLIENT_KEY>foo</AZURE_CLIENT_KEY>
|
||||
<CLIENT_ENDPOINT>https://localhost:8443/</CLIENT_ENDPOINT>
|
||||
<PLUGIN_DIR>../../../</PLUGIN_DIR>
|
||||
</environmentVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
|
||||
</project>
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
KEYTOOL_ARGS="-noprompt -srcstorepass password -deststorepass password -srckeypass password"
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
cd $SCRIPT_DIR
|
||||
|
||||
if [ -f client.keystore ]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
rm client.keystore client.truststore server.keystore server.truststore || true
|
||||
|
||||
openssl genrsa -out diagserverCA.key 2048
|
||||
openssl req -x509 -new -nodes -key diagserverCA.key -sha256 -days 1024 -out diagserverCA.pem -subj "/C=US/ST=NA/L=NA/O=NA/OU=NA/CN=localhost"
|
||||
openssl pkcs12 -export -name server-cert -password pass:password -in diagserverCA.pem -inkey diagserverCA.key -out serverkeystore.p12
|
||||
|
||||
openssl genrsa -out diagclientCA.key 2048
|
||||
openssl req -x509 -new -nodes -key diagclientCA.key -sha256 -days 1024 -out diagclientCA.pem -subj "/C=US/ST=NA/L=NA/O=NA/OU=NA/CN=localhost"
|
||||
openssl pkcs12 -export -name client-cert -password pass:password -in diagclientCA.pem -inkey diagclientCA.key -out clientkeystore.p12
|
||||
|
||||
|
||||
keytool -importkeystore $KEYTOOL_ARGS -destkeystore server.keystore -srckeystore serverkeystore.p12 -srcstoretype pkcs12 -alias server-cert
|
||||
keytool -import $KEYTOOL_ARGS -alias client-cert -file diagclientCA.pem -keystore server.truststore
|
||||
keytool -import $KEYTOOL_ARGS -alias server-cert -file diagserverCA.pem -keystore server.truststore
|
||||
|
||||
|
||||
keytool -importkeystore $KEYTOOL_ARGS -destkeystore client.keystore -srckeystore clientkeystore.p12 -srcstoretype pkcs12 -alias client-cert
|
||||
keytool -import $KEYTOOL_ARGS -alias server-cert -file diagserverCA.pem -keystore client.truststore
|
||||
keytool -import $KEYTOOL_ARGS -alias client-cert -file diagclientCA.pem -keystore client.truststore
|
||||
|
||||
keytool -importkeystore -srckeystore /usr/lib/jvm/default-java/lib/security/cacerts -destkeystore client.truststore -srcstorepass changeit -deststorepass password
|
||||
keytool -importkeystore -srckeystore /usr/lib/jvm/default-java/lib/security/cacerts -destkeystore server.truststore -srcstorepass changeit -deststorepass password
|
||||
|
||||
rm diagclientCA.key diagserverCA.pem clientkeystore.p12 diagclientCA.pem serverkeystore.p12 diagserverCA.key || true
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run this from the api-test/integration-tests directory
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
ROOT_DIR="$SCRIPT_DIR/.."
|
||||
|
||||
cd $SCRIPT_DIR
|
||||
|
||||
if [ ! -f client.truststore ]; then
|
||||
./generateCert.sh
|
||||
fi
|
||||
|
||||
rm -r "$ROOT_DIR/target/wiremock/mappings" || true
|
||||
rm -r "$ROOT_DIR/src/test/resources/wiremock/mappings/" || true
|
||||
mkdir -p "$ROOT_DIR/target/wiremock/mappings"
|
||||
mkdir -p "$ROOT_DIR/src/test/resources/wiremock/mappings/"
|
||||
|
||||
cd $ROOT_DIR
|
||||
source "$ROOT_DIR/../../.env.record"
|
||||
export AZURE_CLIENT_KEY
|
||||
export CLIENT_ENDPOINT
|
||||
export PLUGIN_DIR="$ROOT_DIR/../../../"
|
||||
|
||||
../../mvnw clean package -DskipTests -Pcompile-jdk17
|
||||
|
||||
MAVEN_OPTS="-Djavax.net.ssl.trustStore=scripts/client.truststore -Djavax.net.ssl.trustStorePassword=password" \
|
||||
../../mvnw exec:java@recordmappings -Pcompile-jdk17
|
||||
|
||||
for f in $ROOT_DIR/target/wiremock/mappings/*.json; do
|
||||
cat $f | jq 'del(.response.headers[])' > /tmp/mapping.json
|
||||
cat /tmp/mapping.json | json_pp > $f
|
||||
mv $f src/test/resources/wiremock/mappings/
|
||||
done
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
ROOT_DIR="$SCRIPT_DIR/.."
|
||||
|
||||
cd $SCRIPT_DIR
|
||||
|
||||
if [ ! -f client.truststore ]; then
|
||||
./generateCert.sh
|
||||
fi
|
||||
|
||||
|
||||
cd $ROOT_DIR
|
||||
|
||||
../../mvnw clean test -Dtest=WiremockExamplesIT -Prun-wiremocks
|
||||
-721
@@ -1,721 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.memory.MemoryException;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecord;
|
||||
import com.microsoft.semantickernel.memory.MemoryStore;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
public class JDBCMemoryStoreTest {
|
||||
|
||||
private static JDBCMemoryStore.Builder builder;
|
||||
private static MemoryStore db;
|
||||
private static int collectionNum = 0;
|
||||
private static final String NULL_ADDITIONAL_METADATA = null;
|
||||
private static final String NULL_KEY = null;
|
||||
private static final ZonedDateTime NULL_TIMESTAMP = null;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() throws SQLException {
|
||||
builder =
|
||||
new JDBCMemoryStore.Builder()
|
||||
.withConnection(DriverManager.getConnection("jdbc:sqlite::memory:"));
|
||||
db = builder.buildAsync().block();
|
||||
}
|
||||
|
||||
private Collection<MemoryRecord> createBatchRecords(int numRecords) {
|
||||
assertTrue(numRecords % 2 == 0, "Number of records must be even");
|
||||
assertTrue(numRecords > 0, "Number of records must be greater than 0");
|
||||
|
||||
List<MemoryRecord> records = new ArrayList<>(numRecords);
|
||||
for (int i = 0; i < numRecords / 2; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
records.add(testRecord);
|
||||
}
|
||||
|
||||
for (int i = numRecords / 2; i < numRecords; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.referenceRecord(
|
||||
"test" + i,
|
||||
"sourceName" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
records.add(testRecord);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@Test
|
||||
void initializeDbConnectionSucceeds() {
|
||||
assertNotNull(db);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itDoesNotFailWithExistingTables() {
|
||||
// Act
|
||||
db = builder.buildAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(db);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanCreateAndGetCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(collections);
|
||||
assertFalse(collections.isEmpty());
|
||||
assertTrue(collections.contains(collection));
|
||||
}
|
||||
|
||||
@Test
|
||||
void itHandlesExceptionsWhenCreatingCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = null;
|
||||
|
||||
// Assert
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> db.createCollectionAsync(collection).block(),
|
||||
"Should not be able to create collection with null name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCannotInsertIntoNonExistentCollectionAsync() {
|
||||
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Assert
|
||||
assertThrows(
|
||||
MemoryException.class,
|
||||
() -> db.upsertAsync(collection, testRecord).block(),
|
||||
"Should not be able to insert into a non-existent collection");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actualDefault = db.getAsync(collection, key, false).block();
|
||||
MemoryRecord actualWithEmbedding = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actualDefault);
|
||||
assertNotNull(actualDefault.getEmbedding());
|
||||
assertNotNull(actualDefault.getEmbedding().getVector());
|
||||
assertTrue(actualDefault.getEmbedding().getVector().isEmpty());
|
||||
assertNotNull(actualWithEmbedding);
|
||||
assertNotNull(actualWithEmbedding.getEmbedding());
|
||||
assertNotNull(actualWithEmbedding.getEmbedding().getVector());
|
||||
assertFalse(actualWithEmbedding.getEmbedding().getVector().isEmpty());
|
||||
assertNotEquals(testRecord, actualDefault);
|
||||
assertEquals(testRecord.getMetadata(), actualWithEmbedding.getMetadata());
|
||||
assertEquals(
|
||||
testRecord.getEmbedding().getVector(),
|
||||
actualWithEmbedding.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanUpsertAndRetrieveARecordWithNoTimestampAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertEquals(testRecord.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanUpsertAndRetrieveARecordWithTimestampAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
ZonedDateTime.now(ZoneId.of("UTC")));
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertEquals(testRecord.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertReplacesExistingRecordWithSameIdAsync() {
|
||||
// Arrange
|
||||
String commonId = "test";
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
commonId,
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
MemoryRecord testRecord2 =
|
||||
MemoryRecord.localRecord(
|
||||
commonId,
|
||||
"text2",
|
||||
"description2",
|
||||
new Embedding(Arrays.asList(1f, 2f, 4f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
String key2 = db.upsertAsync(collection, testRecord2).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertNotEquals(testRecord2, actual);
|
||||
assertEquals(key, key2);
|
||||
assertEquals(testRecord2.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord2.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingRecordCanBeRemovedAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
assertNotNull(key);
|
||||
db.removeAsync(collection, key).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingNonExistingRecordDoesNothingAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.removeAsync(collection, "key").block();
|
||||
MemoryRecord actual = db.getAsync(collection, "key", false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanListAllDatabaseCollectionsAsync() {
|
||||
// Arrange
|
||||
int numCollections = 3;
|
||||
String[] testCollections =
|
||||
IntStream.range(collectionNum, collectionNum += numCollections)
|
||||
.mapToObj(i -> "test_collection" + i)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
int initialSize = collections.size();
|
||||
|
||||
Flux.fromArray(testCollections)
|
||||
.concatMap(collection -> db.createCollectionAsync(collection))
|
||||
.blockLast();
|
||||
|
||||
// Act
|
||||
collections = db.getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(collections);
|
||||
assertEquals(initialSize + numCollections, collections.size());
|
||||
for (String collection : testCollections) {
|
||||
assertTrue(
|
||||
collections.contains(collection),
|
||||
"Collections does not contain the newly-created collection " + collection);
|
||||
}
|
||||
}
|
||||
|
||||
private String setUpNearestMatches() {
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
int i = 0;
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(-1f, -1f, -1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(-1f, -2f, -3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, -1f, -2f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsAllResultsWithNoMinScoreAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, topN, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertEquals(topN, topNResults.size());
|
||||
Tuple2<MemoryRecord, Float>[] topNResultsArray = topNResults.toArray(new Tuple2[0]);
|
||||
for (int j = 0; j < topN - 1; j++) {
|
||||
int compare =
|
||||
Double.compare(topNResultsArray[j].getT2(), topNResultsArray[j + 1].getT2());
|
||||
assertTrue(compare >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsLimit() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, 0, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsEmptyIfLimitZero() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, 0, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsEmptyIfCollectionEmpty() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(
|
||||
collection, compareEmbedding, Integer.MAX_VALUE, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = 0.75f;
|
||||
Tuple2<MemoryRecord, Float> topNResultDefault =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
Tuple2<MemoryRecord, Float> topNResultWithEmbedding =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResultDefault);
|
||||
assertNotNull(topNResultWithEmbedding);
|
||||
assertNotNull(topNResultDefault.getT1().getEmbedding());
|
||||
assertNotNull(topNResultDefault.getT1().getEmbedding().getVector());
|
||||
assertTrue(topNResultDefault.getT1().getEmbedding().getVector().isEmpty());
|
||||
assertNotNull(topNResultWithEmbedding.getT1().getEmbedding());
|
||||
assertNotNull(topNResultWithEmbedding.getT1().getEmbedding().getVector());
|
||||
assertFalse(topNResultWithEmbedding.getT1().getEmbedding().getVector().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchAsyncReturnsExpectedAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = 0.75f;
|
||||
Tuple2<MemoryRecord, Float> topNResult =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResult);
|
||||
assertEquals("test0", topNResult.getT1().getMetadata().getId());
|
||||
assertTrue(topNResult.getT2() >= threshold);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatcheReturnsEmptyIfCollectionEmpty() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Tuple2<MemoryRecord, Float> topNResults =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(topNResults);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesDifferentiatesIdenticalVectorsByKeyAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
}
|
||||
|
||||
// Act
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, topN, 0.75f, true).block();
|
||||
Collection<String> topNKeys =
|
||||
topNResults.stream()
|
||||
.map(tuple -> tuple.getT1().getKey())
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Assert
|
||||
assertEquals(topN, topNResults.size());
|
||||
assertEquals(topN, topNKeys.size());
|
||||
for (Iterator<Tuple2<MemoryRecord, Float>> iterator = topNResults.iterator();
|
||||
iterator.hasNext(); ) {
|
||||
Tuple2<MemoryRecord, Float> tuple = iterator.next();
|
||||
int compare = Float.compare(tuple.getT2(), 0.75f);
|
||||
assertTrue(topNKeys.contains(tuple.getT1().getKey()));
|
||||
assertTrue(compare >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchUpsertRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
|
||||
// Act
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
Collection<MemoryRecord> resultRecords = db.getBatchAsync(collection, keys, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(keys);
|
||||
assertEquals(numRecords, keys.size());
|
||||
assertEquals(numRecords, resultRecords.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchGetRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
|
||||
// Act
|
||||
Collection<MemoryRecord> results = db.getBatchAsync(collection, keys, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(keys);
|
||||
assertNotNull(results);
|
||||
assertEquals(numRecords, results.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchRemoveRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (String key : db.upsertBatchAsync(collection, records).block()) {
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
// Act
|
||||
db.removeBatchAsync(collection, keys).block();
|
||||
|
||||
// Assert
|
||||
for (MemoryRecord result : db.getBatchAsync(collection, keys, true).block()) {
|
||||
assertNull(result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectionsCanBeDeletedAsync() {
|
||||
// Arrange
|
||||
int numCollections = 3;
|
||||
String[] testCollections =
|
||||
IntStream.range(collectionNum, collectionNum += numCollections)
|
||||
.mapToObj(i -> "test_collection" + i)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
int initialSize = collections.size();
|
||||
|
||||
Flux.fromArray(testCollections)
|
||||
.concatMap(collection -> db.createCollectionAsync(collection))
|
||||
.blockLast();
|
||||
collectionNum += numCollections;
|
||||
|
||||
// Act
|
||||
collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
assertEquals(initialSize + numCollections, collections.size());
|
||||
|
||||
// Act
|
||||
for (String collection : collections) {
|
||||
db.deleteCollectionAsync(collection).block();
|
||||
}
|
||||
|
||||
// Assert
|
||||
collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
assertEquals(0, collections.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itThrowsWhenDeletingNonExistentCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
assertThrows(MemoryException.class, () -> db.deleteCollectionAsync(collection).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesCollectionExistAsyncReturnTrueForExistingCollection() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
assertTrue(db.doesCollectionExistAsync(collection).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesCollectionExistAsyncReturnFalseForNonExistentCollection() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
assertFalse(db.doesCollectionExistAsync(collection).block());
|
||||
}
|
||||
}
|
||||
-731
@@ -1,731 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.mysql;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.memory.MemoryException;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecord;
|
||||
import com.microsoft.semantickernel.memory.MemoryStore;
|
||||
import java.io.IOException;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.MySQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
@Testcontainers
|
||||
public class MySQLMemoryStoreTest {
|
||||
@Container private static final MySQLContainer CONTAINER = new MySQLContainer();
|
||||
private static final String MYSQL_USER = "test";
|
||||
private static final String MYSQL_PASSWORD = "test";
|
||||
|
||||
private static MySQLMemoryStore.Builder builder;
|
||||
private static MemoryStore db;
|
||||
private static int collectionNum = 0;
|
||||
private static final String NULL_ADDITIONAL_METADATA = null;
|
||||
private static final String NULL_KEY = null;
|
||||
private static final ZonedDateTime NULL_TIMESTAMP = null;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() throws SQLException {
|
||||
builder =
|
||||
new MySQLMemoryStore.Builder()
|
||||
.withConnection(
|
||||
DriverManager.getConnection(
|
||||
CONTAINER.getJdbcUrl(), MYSQL_USER, MYSQL_PASSWORD));
|
||||
db = builder.buildAsync().block();
|
||||
}
|
||||
|
||||
private Collection<MemoryRecord> createBatchRecords(int numRecords) {
|
||||
assertTrue(numRecords % 2 == 0, "Number of records must be even");
|
||||
assertTrue(numRecords > 0, "Number of records must be greater than 0");
|
||||
|
||||
List<MemoryRecord> records = new ArrayList<>(numRecords);
|
||||
for (int i = 0; i < numRecords / 2; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
records.add(testRecord);
|
||||
}
|
||||
|
||||
for (int i = numRecords / 2; i < numRecords; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.referenceRecord(
|
||||
"test" + i,
|
||||
"sourceName" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
records.add(testRecord);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@Test
|
||||
void initializeDbConnectionSucceeds() {
|
||||
assertNotNull(db);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itDoesNotFailWithExistingTables() {
|
||||
// Act
|
||||
db = builder.buildAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(db);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanCreateAndGetCollectionAsync() throws IOException {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(collections);
|
||||
assertFalse(collections.isEmpty());
|
||||
assertTrue(collections.contains(collection));
|
||||
}
|
||||
|
||||
@Test
|
||||
void itHandlesExceptionsWhenCreatingCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = null;
|
||||
|
||||
// Assert
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> db.createCollectionAsync(collection).block(),
|
||||
"Should not be able to create collection with null name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCannotInsertIntoNonExistentCollectionAsync() {
|
||||
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Assert
|
||||
assertThrows(
|
||||
MemoryException.class,
|
||||
() -> db.upsertAsync(collection, testRecord).block(),
|
||||
"Should not be able to insert into a non-existent collection");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actualDefault = db.getAsync(collection, key, false).block();
|
||||
MemoryRecord actualWithEmbedding = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actualDefault);
|
||||
assertNotNull(actualDefault.getEmbedding());
|
||||
assertNotNull(actualDefault.getEmbedding().getVector());
|
||||
assertTrue(actualDefault.getEmbedding().getVector().isEmpty());
|
||||
assertNotNull(actualWithEmbedding);
|
||||
assertNotNull(actualWithEmbedding.getEmbedding());
|
||||
assertNotNull(actualWithEmbedding.getEmbedding().getVector());
|
||||
assertFalse(actualWithEmbedding.getEmbedding().getVector().isEmpty());
|
||||
assertNotEquals(testRecord, actualDefault);
|
||||
assertEquals(testRecord.getMetadata(), actualWithEmbedding.getMetadata());
|
||||
assertEquals(
|
||||
testRecord.getEmbedding().getVector(),
|
||||
actualWithEmbedding.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanUpsertAndRetrieveARecordWithNoTimestampAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertEquals(testRecord.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanUpsertAndRetrieveARecordWithTimestampAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
ZonedDateTime.now(ZoneId.of("UTC")));
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertEquals(testRecord.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertReplacesExistingRecordWithSameIdAsync() {
|
||||
// Arrange
|
||||
String commonId = "test";
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
commonId,
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
MemoryRecord testRecord2 =
|
||||
MemoryRecord.localRecord(
|
||||
commonId,
|
||||
"text2",
|
||||
"description2",
|
||||
new Embedding(Arrays.asList(1f, 2f, 4f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
String key2 = db.upsertAsync(collection, testRecord2).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertNotEquals(testRecord2, actual);
|
||||
assertEquals(key, key2);
|
||||
assertEquals(testRecord2.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord2.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingRecordCanBeRemovedAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
assertNotNull(key);
|
||||
db.removeAsync(collection, key).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingNonExistingRecordDoesNothingAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.removeAsync(collection, "key").block();
|
||||
MemoryRecord actual = db.getAsync(collection, "key", false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanListAllDatabaseCollectionsAsync() {
|
||||
// Arrange
|
||||
int numCollections = 3;
|
||||
String[] testCollections =
|
||||
IntStream.range(collectionNum, collectionNum += numCollections)
|
||||
.mapToObj(i -> "test_collection" + i)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
int initialSize = collections.size();
|
||||
|
||||
Flux.fromArray(testCollections)
|
||||
.concatMap(collection -> db.createCollectionAsync(collection))
|
||||
.blockLast();
|
||||
|
||||
// Act
|
||||
collections = db.getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(collections);
|
||||
assertEquals(initialSize + numCollections, collections.size());
|
||||
for (String collection : testCollections) {
|
||||
assertTrue(
|
||||
collections.contains(collection),
|
||||
"Collections does not contain the newly-created collection " + collection);
|
||||
}
|
||||
}
|
||||
|
||||
private String setUpNearestMatches() {
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
int i = 0;
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(-1f, -1f, -1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(-1f, -2f, -3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, -1f, -2f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsAllResultsWithNoMinScoreAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, topN, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertEquals(topN, topNResults.size());
|
||||
Tuple2<MemoryRecord, Float>[] topNResultsArray = topNResults.toArray(new Tuple2[0]);
|
||||
for (int j = 0; j < topN - 1; j++) {
|
||||
int compare =
|
||||
Double.compare(topNResultsArray[j].getT2(), topNResultsArray[j + 1].getT2());
|
||||
assertTrue(compare >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsLimit() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, 0, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsEmptyIfLimitZero() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, 0, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsEmptyIfCollectionEmpty() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(
|
||||
collection, compareEmbedding, Integer.MAX_VALUE, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = 0.75f;
|
||||
Tuple2<MemoryRecord, Float> topNResultDefault =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
Tuple2<MemoryRecord, Float> topNResultWithEmbedding =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResultDefault);
|
||||
assertNotNull(topNResultWithEmbedding);
|
||||
assertNotNull(topNResultDefault.getT1().getEmbedding());
|
||||
assertNotNull(topNResultDefault.getT1().getEmbedding().getVector());
|
||||
assertTrue(topNResultDefault.getT1().getEmbedding().getVector().isEmpty());
|
||||
assertNotNull(topNResultWithEmbedding.getT1().getEmbedding());
|
||||
assertNotNull(topNResultWithEmbedding.getT1().getEmbedding().getVector());
|
||||
assertFalse(topNResultWithEmbedding.getT1().getEmbedding().getVector().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchAsyncReturnsExpectedAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = 0.75f;
|
||||
Tuple2<MemoryRecord, Float> topNResult =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResult);
|
||||
assertEquals("test0", topNResult.getT1().getMetadata().getId());
|
||||
assertTrue(topNResult.getT2() >= threshold);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatcheReturnsEmptyIfCollectionEmpty() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Tuple2<MemoryRecord, Float> topNResults =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(topNResults);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesDifferentiatesIdenticalVectorsByKeyAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
}
|
||||
|
||||
// Act
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, topN, 0.75f, true).block();
|
||||
Collection<String> topNKeys =
|
||||
topNResults.stream()
|
||||
.map(tuple -> tuple.getT1().getKey())
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Assert
|
||||
assertEquals(topN, topNResults.size());
|
||||
assertEquals(topN, topNKeys.size());
|
||||
for (Iterator<Tuple2<MemoryRecord, Float>> iterator = topNResults.iterator();
|
||||
iterator.hasNext(); ) {
|
||||
Tuple2<MemoryRecord, Float> tuple = iterator.next();
|
||||
int compare = Float.compare(tuple.getT2(), 0.75f);
|
||||
assertTrue(topNKeys.contains(tuple.getT1().getKey()));
|
||||
assertTrue(compare >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchUpsertRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
|
||||
// Act
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
Collection<MemoryRecord> resultRecords = db.getBatchAsync(collection, keys, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(keys);
|
||||
assertEquals(numRecords, keys.size());
|
||||
assertEquals(numRecords, resultRecords.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchGetRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
|
||||
// Act
|
||||
Collection<MemoryRecord> results = db.getBatchAsync(collection, keys, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(keys);
|
||||
assertNotNull(results);
|
||||
assertEquals(numRecords, results.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchRemoveRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (String key : db.upsertBatchAsync(collection, records).block()) {
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
// Act
|
||||
db.removeBatchAsync(collection, keys).block();
|
||||
|
||||
// Assert
|
||||
for (MemoryRecord result : db.getBatchAsync(collection, keys, true).block()) {
|
||||
assertNull(result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectionsCanBeDeletedAsync() {
|
||||
// Arrange
|
||||
int numCollections = 3;
|
||||
String[] testCollections =
|
||||
IntStream.range(collectionNum, collectionNum += numCollections)
|
||||
.mapToObj(i -> "test_collection" + i)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
int initialSize = collections.size();
|
||||
|
||||
Flux.fromArray(testCollections)
|
||||
.concatMap(collection -> db.createCollectionAsync(collection))
|
||||
.blockLast();
|
||||
collectionNum += numCollections;
|
||||
|
||||
// Act
|
||||
collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
assertEquals(initialSize + numCollections, collections.size());
|
||||
|
||||
// Act
|
||||
for (String collection : collections) {
|
||||
db.deleteCollectionAsync(collection).block();
|
||||
}
|
||||
|
||||
// Assert
|
||||
collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
assertEquals(0, collections.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itThrowsWhenDeletingNonExistentCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
assertThrows(MemoryException.class, () -> db.deleteCollectionAsync(collection).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesCollectionExistAsyncReturnTrueForExistingCollection() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
assertTrue(db.doesCollectionExistAsync(collection).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesCollectionExistAsyncReturnFalseForNonExistentCollection() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
assertFalse(db.doesCollectionExistAsync(collection).block());
|
||||
}
|
||||
}
|
||||
-731
@@ -1,731 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.postgresql;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.memory.MemoryException;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecord;
|
||||
import com.microsoft.semantickernel.memory.MemoryStore;
|
||||
import java.io.IOException;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
@Testcontainers
|
||||
public class PostgreSQLMemoryStoreTest {
|
||||
@Container private static final PostgreSQLContainer CONTAINER = new PostgreSQLContainer();
|
||||
private static final String POSTGRES_USER = "test";
|
||||
private static final String POSTGRES_PASSWORD = "test";
|
||||
|
||||
private static PostgreSQLMemoryStore.Builder builder;
|
||||
private static MemoryStore db;
|
||||
private static int collectionNum = 0;
|
||||
private static final String NULL_ADDITIONAL_METADATA = null;
|
||||
private static final String NULL_KEY = null;
|
||||
private static final ZonedDateTime NULL_TIMESTAMP = null;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() throws SQLException {
|
||||
builder =
|
||||
new PostgreSQLMemoryStore.Builder()
|
||||
.withConnection(
|
||||
DriverManager.getConnection(
|
||||
CONTAINER.getJdbcUrl(), POSTGRES_USER, POSTGRES_PASSWORD));
|
||||
db = builder.buildAsync().block();
|
||||
}
|
||||
|
||||
private Collection<MemoryRecord> createBatchRecords(int numRecords) {
|
||||
assertTrue(numRecords % 2 == 0, "Number of records must be even");
|
||||
assertTrue(numRecords > 0, "Number of records must be greater than 0");
|
||||
|
||||
List<MemoryRecord> records = new ArrayList<>(numRecords);
|
||||
for (int i = 0; i < numRecords / 2; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
records.add(testRecord);
|
||||
}
|
||||
|
||||
for (int i = numRecords / 2; i < numRecords; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.referenceRecord(
|
||||
"test" + i,
|
||||
"sourceName" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
records.add(testRecord);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@Test
|
||||
void initializeDbConnectionSucceeds() {
|
||||
assertNotNull(db);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itDoesNotFailWithExistingTables() {
|
||||
// Act
|
||||
db = builder.buildAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(db);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanCreateAndGetCollectionAsync() throws IOException {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(collections);
|
||||
assertFalse(collections.isEmpty());
|
||||
assertTrue(collections.contains(collection));
|
||||
}
|
||||
|
||||
@Test
|
||||
void itHandlesExceptionsWhenCreatingCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = null;
|
||||
|
||||
// Assert
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> db.createCollectionAsync(collection).block(),
|
||||
"Should not be able to create collection with null name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCannotInsertIntoNonExistentCollectionAsync() {
|
||||
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Assert
|
||||
assertThrows(
|
||||
MemoryException.class,
|
||||
() -> db.upsertAsync(collection, testRecord).block(),
|
||||
"Should not be able to insert into a non-existent collection");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actualDefault = db.getAsync(collection, key, false).block();
|
||||
MemoryRecord actualWithEmbedding = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actualDefault);
|
||||
assertNotNull(actualDefault.getEmbedding());
|
||||
assertNotNull(actualDefault.getEmbedding().getVector());
|
||||
assertTrue(actualDefault.getEmbedding().getVector().isEmpty());
|
||||
assertNotNull(actualWithEmbedding);
|
||||
assertNotNull(actualWithEmbedding.getEmbedding());
|
||||
assertNotNull(actualWithEmbedding.getEmbedding().getVector());
|
||||
assertFalse(actualWithEmbedding.getEmbedding().getVector().isEmpty());
|
||||
assertNotEquals(testRecord, actualDefault);
|
||||
assertEquals(testRecord.getMetadata(), actualWithEmbedding.getMetadata());
|
||||
assertEquals(
|
||||
testRecord.getEmbedding().getVector(),
|
||||
actualWithEmbedding.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanUpsertAndRetrieveARecordWithNoTimestampAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertEquals(testRecord.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanUpsertAndRetrieveARecordWithTimestampAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
ZonedDateTime.now(ZoneId.of("UTC")));
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertEquals(testRecord.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertReplacesExistingRecordWithSameIdAsync() {
|
||||
// Arrange
|
||||
String commonId = "test";
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
commonId,
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
MemoryRecord testRecord2 =
|
||||
MemoryRecord.localRecord(
|
||||
commonId,
|
||||
"text2",
|
||||
"description2",
|
||||
new Embedding(Arrays.asList(1f, 2f, 4f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
String key2 = db.upsertAsync(collection, testRecord2).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertNotEquals(testRecord2, actual);
|
||||
assertEquals(key, key2);
|
||||
assertEquals(testRecord2.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord2.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingRecordCanBeRemovedAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
assertNotNull(key);
|
||||
db.removeAsync(collection, key).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingNonExistingRecordDoesNothingAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.removeAsync(collection, "key").block();
|
||||
MemoryRecord actual = db.getAsync(collection, "key", false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanListAllDatabaseCollectionsAsync() {
|
||||
// Arrange
|
||||
int numCollections = 3;
|
||||
String[] testCollections =
|
||||
IntStream.range(collectionNum, collectionNum += numCollections)
|
||||
.mapToObj(i -> "test_collection" + i)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
int initialSize = collections.size();
|
||||
|
||||
Flux.fromArray(testCollections)
|
||||
.concatMap(collection -> db.createCollectionAsync(collection))
|
||||
.blockLast();
|
||||
|
||||
// Act
|
||||
collections = db.getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(collections);
|
||||
assertEquals(initialSize + numCollections, collections.size());
|
||||
for (String collection : testCollections) {
|
||||
assertTrue(
|
||||
collections.contains(collection),
|
||||
"Collections does not contain the newly-created collection " + collection);
|
||||
}
|
||||
}
|
||||
|
||||
private String setUpNearestMatches() {
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
int i = 0;
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(-1f, -1f, -1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(-1f, -2f, -3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, -1f, -2f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsAllResultsWithNoMinScoreAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, topN, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertEquals(topN, topNResults.size());
|
||||
Tuple2<MemoryRecord, Float>[] topNResultsArray = topNResults.toArray(new Tuple2[0]);
|
||||
for (int j = 0; j < topN - 1; j++) {
|
||||
int compare =
|
||||
Double.compare(topNResultsArray[j].getT2(), topNResultsArray[j + 1].getT2());
|
||||
assertTrue(compare >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsLimit() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, 0, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsEmptyIfLimitZero() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, 0, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsEmptyIfCollectionEmpty() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(
|
||||
collection, compareEmbedding, Integer.MAX_VALUE, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = 0.75f;
|
||||
Tuple2<MemoryRecord, Float> topNResultDefault =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
Tuple2<MemoryRecord, Float> topNResultWithEmbedding =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResultDefault);
|
||||
assertNotNull(topNResultWithEmbedding);
|
||||
assertNotNull(topNResultDefault.getT1().getEmbedding());
|
||||
assertNotNull(topNResultDefault.getT1().getEmbedding().getVector());
|
||||
assertTrue(topNResultDefault.getT1().getEmbedding().getVector().isEmpty());
|
||||
assertNotNull(topNResultWithEmbedding.getT1().getEmbedding());
|
||||
assertNotNull(topNResultWithEmbedding.getT1().getEmbedding().getVector());
|
||||
assertFalse(topNResultWithEmbedding.getT1().getEmbedding().getVector().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchAsyncReturnsExpectedAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = 0.75f;
|
||||
Tuple2<MemoryRecord, Float> topNResult =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResult);
|
||||
assertEquals("test0", topNResult.getT1().getMetadata().getId());
|
||||
assertTrue(topNResult.getT2() >= threshold);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatcheReturnsEmptyIfCollectionEmpty() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Tuple2<MemoryRecord, Float> topNResults =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(topNResults);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesDifferentiatesIdenticalVectorsByKeyAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
}
|
||||
|
||||
// Act
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, topN, 0.75f, true).block();
|
||||
Collection<String> topNKeys =
|
||||
topNResults.stream()
|
||||
.map(tuple -> tuple.getT1().getKey())
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Assert
|
||||
assertEquals(topN, topNResults.size());
|
||||
assertEquals(topN, topNKeys.size());
|
||||
for (Iterator<Tuple2<MemoryRecord, Float>> iterator = topNResults.iterator();
|
||||
iterator.hasNext(); ) {
|
||||
Tuple2<MemoryRecord, Float> tuple = iterator.next();
|
||||
int compare = Float.compare(tuple.getT2(), 0.75f);
|
||||
assertTrue(topNKeys.contains(tuple.getT1().getKey()));
|
||||
assertTrue(compare >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchUpsertRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
|
||||
// Act
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
Collection<MemoryRecord> resultRecords = db.getBatchAsync(collection, keys, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(keys);
|
||||
assertEquals(numRecords, keys.size());
|
||||
assertEquals(numRecords, resultRecords.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchGetRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
|
||||
// Act
|
||||
Collection<MemoryRecord> results = db.getBatchAsync(collection, keys, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(keys);
|
||||
assertNotNull(results);
|
||||
assertEquals(numRecords, results.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchRemoveRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (String key : db.upsertBatchAsync(collection, records).block()) {
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
// Act
|
||||
db.removeBatchAsync(collection, keys).block();
|
||||
|
||||
// Assert
|
||||
for (MemoryRecord result : db.getBatchAsync(collection, keys, true).block()) {
|
||||
assertNull(result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectionsCanBeDeletedAsync() {
|
||||
// Arrange
|
||||
int numCollections = 3;
|
||||
String[] testCollections =
|
||||
IntStream.range(collectionNum, collectionNum += numCollections)
|
||||
.mapToObj(i -> "test_collection" + i)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
int initialSize = collections.size();
|
||||
|
||||
Flux.fromArray(testCollections)
|
||||
.concatMap(collection -> db.createCollectionAsync(collection))
|
||||
.blockLast();
|
||||
collectionNum += numCollections;
|
||||
|
||||
// Act
|
||||
collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
assertEquals(initialSize + numCollections, collections.size());
|
||||
|
||||
// Act
|
||||
for (String collection : collections) {
|
||||
db.deleteCollectionAsync(collection).block();
|
||||
}
|
||||
|
||||
// Assert
|
||||
collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
assertEquals(0, collections.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itThrowsWhenDeletingNonExistentCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
assertThrows(MemoryException.class, () -> db.deleteCollectionAsync(collection).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesCollectionExistAsyncReturnTrueForExistingCollection() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
assertTrue(db.doesCollectionExistAsync(collection).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesCollectionExistAsyncReturnFalseForNonExistentCollection() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
assertFalse(db.doesCollectionExistAsync(collection).block());
|
||||
}
|
||||
}
|
||||
-719
@@ -1,719 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.sqlite;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.JDBCMemoryStoreTest;
|
||||
import com.microsoft.semantickernel.memory.MemoryException;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecord;
|
||||
import com.microsoft.semantickernel.memory.MemoryStore;
|
||||
import java.sql.SQLException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
public class SQLiteMemoryStoreTest extends JDBCMemoryStoreTest {
|
||||
|
||||
private static SQLiteMemoryStore.Builder builder;
|
||||
private static MemoryStore db;
|
||||
private static int collectionNum = 0;
|
||||
private static final String NULL_ADDITIONAL_METADATA = null;
|
||||
private static final String NULL_KEY = null;
|
||||
private static final ZonedDateTime NULL_TIMESTAMP = null;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() throws SQLException {
|
||||
builder = new SQLiteMemoryStore.Builder().withFilename(":memory:");
|
||||
db = builder.buildAsync().block();
|
||||
}
|
||||
|
||||
private Collection<MemoryRecord> createBatchRecords(int numRecords) {
|
||||
assertTrue(numRecords % 2 == 0, "Number of records must be even");
|
||||
assertTrue(numRecords > 0, "Number of records must be greater than 0");
|
||||
|
||||
List<MemoryRecord> records = new ArrayList<>(numRecords);
|
||||
for (int i = 0; i < numRecords / 2; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
records.add(testRecord);
|
||||
}
|
||||
|
||||
for (int i = numRecords / 2; i < numRecords; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.referenceRecord(
|
||||
"test" + i,
|
||||
"sourceName" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
records.add(testRecord);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@Test
|
||||
void initializeDbConnectionSucceeds() {
|
||||
assertNotNull(db);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itDoesNotFailWithExistingTables() {
|
||||
// Act
|
||||
db = builder.buildAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(db);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanCreateAndGetCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(collections);
|
||||
assertFalse(collections.isEmpty());
|
||||
assertTrue(collections.contains(collection));
|
||||
}
|
||||
|
||||
@Test
|
||||
void itHandlesExceptionsWhenCreatingCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = null;
|
||||
|
||||
// Assert
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> db.createCollectionAsync(collection).block(),
|
||||
"Should not be able to create collection with null name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCannotInsertIntoNonExistentCollectionAsync() {
|
||||
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Assert
|
||||
assertThrows(
|
||||
MemoryException.class,
|
||||
() -> db.upsertAsync(collection, testRecord).block(),
|
||||
"Should not be able to insert into a non-existent collection");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actualDefault = db.getAsync(collection, key, false).block();
|
||||
MemoryRecord actualWithEmbedding = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actualDefault);
|
||||
assertNotNull(actualDefault.getEmbedding());
|
||||
assertNotNull(actualDefault.getEmbedding().getVector());
|
||||
assertTrue(actualDefault.getEmbedding().getVector().isEmpty());
|
||||
assertNotNull(actualWithEmbedding);
|
||||
assertNotNull(actualWithEmbedding.getEmbedding());
|
||||
assertNotNull(actualWithEmbedding.getEmbedding().getVector());
|
||||
assertFalse(actualWithEmbedding.getEmbedding().getVector().isEmpty());
|
||||
assertNotEquals(testRecord, actualDefault);
|
||||
assertEquals(testRecord.getMetadata(), actualWithEmbedding.getMetadata());
|
||||
assertEquals(
|
||||
testRecord.getEmbedding().getVector(),
|
||||
actualWithEmbedding.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanUpsertAndRetrieveARecordWithNoTimestampAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertEquals(testRecord.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanUpsertAndRetrieveARecordWithTimestampAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
ZonedDateTime.now(ZoneId.of("UTC")));
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertEquals(testRecord.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertReplacesExistingRecordWithSameIdAsync() {
|
||||
// Arrange
|
||||
String commonId = "test";
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
commonId,
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
MemoryRecord testRecord2 =
|
||||
MemoryRecord.localRecord(
|
||||
commonId,
|
||||
"text2",
|
||||
"description2",
|
||||
new Embedding(Arrays.asList(1f, 2f, 4f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
String key2 = db.upsertAsync(collection, testRecord2).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(actual);
|
||||
assertNotEquals(testRecord2, actual);
|
||||
assertEquals(key, key2);
|
||||
assertEquals(testRecord2.getMetadata(), actual.getMetadata());
|
||||
assertEquals(testRecord2.getEmbedding().getVector(), actual.getEmbedding().getVector());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingRecordCanBeRemovedAsync() {
|
||||
// Arrange
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test",
|
||||
"text",
|
||||
"description",
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.createCollectionAsync(collection).block();
|
||||
String key = db.upsertAsync(collection, testRecord).block();
|
||||
assertNotNull(key);
|
||||
db.removeAsync(collection, key).block();
|
||||
MemoryRecord actual = db.getAsync(collection, key, false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingNonExistingRecordDoesNothingAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
db.removeAsync(collection, "key").block();
|
||||
MemoryRecord actual = db.getAsync(collection, "key", false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanListAllDatabaseCollectionsAsync() {
|
||||
// Arrange
|
||||
int numCollections = 3;
|
||||
String[] testCollections =
|
||||
IntStream.range(collectionNum, collectionNum += numCollections)
|
||||
.mapToObj(i -> "test_collection" + i)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
int initialSize = collections.size();
|
||||
|
||||
Flux.fromArray(testCollections)
|
||||
.concatMap(collection -> db.createCollectionAsync(collection))
|
||||
.blockLast();
|
||||
|
||||
// Act
|
||||
collections = db.getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(collections);
|
||||
assertEquals(initialSize + numCollections, collections.size());
|
||||
for (String collection : testCollections) {
|
||||
assertTrue(
|
||||
collections.contains(collection),
|
||||
"Collections does not contain the newly-created collection " + collection);
|
||||
}
|
||||
}
|
||||
|
||||
private String setUpNearestMatches() {
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
int i = 0;
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(-1f, -1f, -1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 2f, 3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(-1f, -2f, -3f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
i++;
|
||||
testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, -1f, -2f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsAllResultsWithNoMinScoreAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, topN, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertEquals(topN, topNResults.size());
|
||||
Tuple2<MemoryRecord, Float>[] topNResultsArray = topNResults.toArray(new Tuple2[0]);
|
||||
for (int j = 0; j < topN - 1; j++) {
|
||||
int compare =
|
||||
Double.compare(topNResultsArray[j].getT2(), topNResultsArray[j + 1].getT2());
|
||||
assertTrue(compare >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsLimit() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, 0, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsEmptyIfLimitZero() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, 0, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesReturnsEmptyIfCollectionEmpty() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(
|
||||
collection, compareEmbedding, Integer.MAX_VALUE, threshold, false)
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResults);
|
||||
assertTrue(topNResults.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchAsyncReturnsEmptyEmbeddingUnlessSpecifiedAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = 0.75f;
|
||||
Tuple2<MemoryRecord, Float> topNResultDefault =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
Tuple2<MemoryRecord, Float> topNResultWithEmbedding =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, true).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResultDefault);
|
||||
assertNotNull(topNResultWithEmbedding);
|
||||
assertNotNull(topNResultDefault.getT1().getEmbedding());
|
||||
assertNotNull(topNResultDefault.getT1().getEmbedding().getVector());
|
||||
assertTrue(topNResultDefault.getT1().getEmbedding().getVector().isEmpty());
|
||||
assertNotNull(topNResultWithEmbedding.getT1().getEmbedding());
|
||||
assertNotNull(topNResultWithEmbedding.getT1().getEmbedding().getVector());
|
||||
assertFalse(topNResultWithEmbedding.getT1().getEmbedding().getVector().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchAsyncReturnsExpectedAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = setUpNearestMatches();
|
||||
|
||||
// Act
|
||||
float threshold = 0.75f;
|
||||
Tuple2<MemoryRecord, Float> topNResult =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(topNResult);
|
||||
assertEquals("test0", topNResult.getT1().getMetadata().getId());
|
||||
assertTrue(topNResult.getT2() >= threshold);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatcheReturnsEmptyIfCollectionEmpty() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
float threshold = -1f;
|
||||
Tuple2<MemoryRecord, Float> topNResults =
|
||||
db.getNearestMatchAsync(collection, compareEmbedding, threshold, false).block();
|
||||
|
||||
// Assert
|
||||
assertNull(topNResults);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestMatchesDifferentiatesIdenticalVectorsByKeyAsync() {
|
||||
// Arrange
|
||||
Embedding compareEmbedding = new Embedding(Arrays.asList(1f, 1f, 1f));
|
||||
int topN = 4;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
MemoryRecord testRecord =
|
||||
MemoryRecord.localRecord(
|
||||
"test" + i,
|
||||
"text" + i,
|
||||
"description" + i,
|
||||
new Embedding(Arrays.asList(1f, 1f, 1f)),
|
||||
NULL_ADDITIONAL_METADATA,
|
||||
NULL_KEY,
|
||||
NULL_TIMESTAMP);
|
||||
db.upsertAsync(collection, testRecord).block();
|
||||
}
|
||||
|
||||
// Act
|
||||
Collection<Tuple2<MemoryRecord, Float>> topNResults =
|
||||
db.getNearestMatchesAsync(collection, compareEmbedding, topN, 0.75f, true).block();
|
||||
Collection<String> topNKeys =
|
||||
topNResults.stream()
|
||||
.map(tuple -> tuple.getT1().getKey())
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Assert
|
||||
assertEquals(topN, topNResults.size());
|
||||
assertEquals(topN, topNKeys.size());
|
||||
for (Iterator<Tuple2<MemoryRecord, Float>> iterator = topNResults.iterator();
|
||||
iterator.hasNext(); ) {
|
||||
Tuple2<MemoryRecord, Float> tuple = iterator.next();
|
||||
int compare = Float.compare(tuple.getT2(), 0.75f);
|
||||
assertTrue(topNKeys.contains(tuple.getT1().getKey()));
|
||||
assertTrue(compare >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchUpsertRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
|
||||
// Act
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
Collection<MemoryRecord> resultRecords = db.getBatchAsync(collection, keys, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(keys);
|
||||
assertEquals(numRecords, keys.size());
|
||||
assertEquals(numRecords, resultRecords.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchGetRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
|
||||
// Act
|
||||
Collection<MemoryRecord> results = db.getBatchAsync(collection, keys, false).block();
|
||||
|
||||
// Assert
|
||||
assertNotNull(keys);
|
||||
assertNotNull(results);
|
||||
assertEquals(numRecords, results.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itCanBatchRemoveRecordsAsync() {
|
||||
// Arrange
|
||||
int numRecords = 10;
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
Collection<MemoryRecord> records = this.createBatchRecords(numRecords);
|
||||
|
||||
Collection<String> keys = db.upsertBatchAsync(collection, records).block();
|
||||
assertNotNull(keys);
|
||||
|
||||
// Act
|
||||
db.removeBatchAsync(collection, keys).block();
|
||||
|
||||
// Assert
|
||||
Collection<MemoryRecord> results = db.getBatchAsync(collection, keys, false).block();
|
||||
assertNotNull(results);
|
||||
for (MemoryRecord result : results) {
|
||||
assertNull(result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectionsCanBeDeletedAsync() {
|
||||
// Arrange
|
||||
int numCollections = 3;
|
||||
String[] testCollections =
|
||||
IntStream.range(collectionNum, collectionNum += numCollections)
|
||||
.mapToObj(i -> "test_collection" + i)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Collection<String> collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
int initialSize = collections.size();
|
||||
|
||||
Flux.fromArray(testCollections)
|
||||
.concatMap(collection -> db.createCollectionAsync(collection))
|
||||
.blockLast();
|
||||
collectionNum += numCollections;
|
||||
|
||||
// Act
|
||||
collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
assertEquals(initialSize + numCollections, collections.size());
|
||||
|
||||
// Act
|
||||
for (String collection : collections) {
|
||||
db.deleteCollectionAsync(collection).block();
|
||||
}
|
||||
|
||||
// Assert
|
||||
collections = db.getCollectionsAsync().block();
|
||||
assertNotNull(collections);
|
||||
assertEquals(0, collections.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itThrowsWhenDeletingNonExistentCollectionAsync() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
assertThrows(MemoryException.class, () -> db.deleteCollectionAsync(collection).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesCollectionExistAsyncReturnTrueForExistingCollection() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
db.createCollectionAsync(collection).block();
|
||||
|
||||
// Act
|
||||
assertTrue(db.doesCollectionExistAsync(collection).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesCollectionExistAsyncReturnFalseForNonExistentCollection() {
|
||||
// Arrange
|
||||
String collection = "test_collection" + collectionNum;
|
||||
collectionNum++;
|
||||
|
||||
// Act
|
||||
assertFalse(db.doesCollectionExistAsync(collection).block());
|
||||
}
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.e2e;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.SamplesConfig;
|
||||
import com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion;
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import com.microsoft.semantickernel.memory.VolatileMemoryStore;
|
||||
import com.microsoft.semantickernel.textcompletion.TextCompletion;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public class AbstractKernelTest {
|
||||
|
||||
public static final String CONF_OPENAI_PROPERTIES = "conf.openai.properties";
|
||||
public static final String AZURE_CONF_PROPERTIES = "conf.properties";
|
||||
|
||||
public static Kernel buildTextCompletionKernel() throws ConfigurationException {
|
||||
String model = "text-davinci-003";
|
||||
final OpenAIAsyncClient openAIClient = getOpenAIClient();
|
||||
TextCompletion textCompletion = new OpenAITextCompletion(openAIClient, model);
|
||||
|
||||
return SKBuilders.kernel()
|
||||
.withDefaultAIService(textCompletion)
|
||||
.withDefaultAIService(
|
||||
SKBuilders.textEmbeddingGeneration()
|
||||
.withOpenAIClient(openAIClient)
|
||||
.withModelId(model)
|
||||
.build())
|
||||
.withMemoryStorage(new VolatileMemoryStore())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static OpenAIAsyncClient getOpenAIClient() throws ConfigurationException {
|
||||
return SamplesConfig.getClient();
|
||||
}
|
||||
|
||||
public static String getOpenAIModel() throws IOException {
|
||||
return getConfigValue(CONF_OPENAI_PROPERTIES, "model");
|
||||
}
|
||||
|
||||
public static String getToken(String configName) throws IOException {
|
||||
return getConfigValue(configName, "token");
|
||||
}
|
||||
|
||||
public static String getEndpoint(String configName) throws IOException {
|
||||
return getConfigValue(configName, "endpoint");
|
||||
}
|
||||
|
||||
private static String getConfigValue(String configName, String propertyName)
|
||||
throws IOException {
|
||||
String home = new File(System.getProperty("user.home")).getAbsolutePath();
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(home + "/.oai/" + configName)) {
|
||||
Properties props = new Properties();
|
||||
props.load(fis);
|
||||
String apiKey = props.getProperty(propertyName);
|
||||
if (apiKey == null) {
|
||||
System.err.println("NO PROPERTY " + propertyName);
|
||||
return "";
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isOpenAIComTestEnabled() {
|
||||
return checkConfig(AbstractKernelTest.CONF_OPENAI_PROPERTIES);
|
||||
}
|
||||
|
||||
public static boolean isAzureTestEnabled() {
|
||||
return checkConfig(AbstractKernelTest.AZURE_CONF_PROPERTIES);
|
||||
}
|
||||
|
||||
private static boolean checkConfig(String confOpenaiProperties) {
|
||||
if (!Boolean.getBoolean("enable_external_tests")
|
||||
&& !System.getProperties().containsKey("intellij.debug.agent")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (AbstractKernelTest.getEndpoint(confOpenaiProperties) == null) {
|
||||
System.out.println("Test disabled due to lack of configured azure endpoint");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AbstractKernelTest.getToken(confOpenaiProperties) == null) {
|
||||
System.out.println("Test disabled due to lack of configured azure token");
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.e2e;
|
||||
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Function;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class ContextVariableFunctionTest extends AbstractKernelTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ContextVariableFunctionTest.class);
|
||||
|
||||
public static void main(String[] args)
|
||||
throws ExecutionException,
|
||||
InterruptedException,
|
||||
TimeoutException,
|
||||
ConfigurationException {
|
||||
new ContextVariableFunctionTest().runContextVariableTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public void runContextVariableTest()
|
||||
throws ExecutionException,
|
||||
InterruptedException,
|
||||
TimeoutException,
|
||||
ConfigurationException {
|
||||
Kernel kernel = buildTextCompletionKernel();
|
||||
|
||||
String prompt =
|
||||
"""
|
||||
ChatBot can have a conversation with you about any topic.
|
||||
It can give explicit instructions or say 'I don't know' if it does not have an answer.
|
||||
|
||||
{{$history}}
|
||||
User: {{$user_input}}
|
||||
ChatBot:\s""";
|
||||
|
||||
CompletionSKFunction chat =
|
||||
kernel.getSemanticFunctionBuilder()
|
||||
.withPromptTemplate(prompt)
|
||||
.withFunctionName("ChatBot")
|
||||
.withRequestSettings(
|
||||
SKBuilders.completionRequestSettings()
|
||||
.temperature(0.7)
|
||||
.topP(0.5)
|
||||
.maxTokens(2000)
|
||||
.frequencyPenalty(0)
|
||||
.presencePenalty(0)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
SKContext readOnlySkContext = SKBuilders.context().withKernel(kernel).build();
|
||||
|
||||
chat("Hi, I'm looking for book suggestions?", chat, readOnlySkContext)
|
||||
.flatMap(
|
||||
chat(
|
||||
"I love history and philosophy, I'd like to learn something new"
|
||||
+ " about Greece, any suggestion?",
|
||||
chat))
|
||||
.flatMap(chat("that sounds interesting, what is it about?", chat))
|
||||
.flatMap(
|
||||
chat(
|
||||
"if I read that book, what exactly will I learn about Greece"
|
||||
+ " history?",
|
||||
chat))
|
||||
.flatMap(
|
||||
chat("could you list some more books I could read about this topic?", chat))
|
||||
.block();
|
||||
}
|
||||
|
||||
private Function<SKContext, Mono<SKContext>> chat(String input, CompletionSKFunction chat) {
|
||||
return (context) -> {
|
||||
try {
|
||||
return chat(input, chat, context);
|
||||
} catch (ExecutionException | InterruptedException | TimeoutException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Mono<SKContext> chat(String input, CompletionSKFunction chat, SKContext context)
|
||||
throws ExecutionException, InterruptedException, TimeoutException {
|
||||
context = context.setVariable("user_input", input);
|
||||
|
||||
LOGGER.info("User:\n" + input);
|
||||
|
||||
SKContext finalContext = context;
|
||||
return chat.invokeAsync(context, null)
|
||||
.map(
|
||||
result -> {
|
||||
LOGGER.info("Bot:\n\t\t" + result.getResult());
|
||||
|
||||
String existingHistoy = finalContext.getVariables().get("history");
|
||||
if (existingHistoy == null) {
|
||||
existingHistoy = "";
|
||||
}
|
||||
existingHistoy +=
|
||||
"\nUser: " + input + "\nChatBot: " + result.getResult() + "\n";
|
||||
return finalContext.setVariable("history", existingHistoy);
|
||||
});
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.e2e;
|
||||
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public class FunSkillTest extends AbstractKernelTest {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FunSkillTest.class);
|
||||
|
||||
@Test
|
||||
public void tellAJoke() throws ConfigurationException {
|
||||
Mono<SKContext> result =
|
||||
buildTextCompletionKernel()
|
||||
.importSkillFromDirectory("FunSkill", "../../../samples/skills")
|
||||
.getFunction("joke", CompletionSKFunction.class)
|
||||
.invokeAsync("time travel to dinosaur age");
|
||||
|
||||
if (result != null) {
|
||||
LOGGER.info(result.block().getResult());
|
||||
}
|
||||
}
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.e2e;
|
||||
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class InlineFunctionTest extends AbstractKernelTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(InlineFunctionTest.class);
|
||||
|
||||
@Test
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public void executeInlineFunction() throws ConfigurationException {
|
||||
Kernel kernel = buildTextCompletionKernel();
|
||||
String prompt = "{{$input}}\n" + "Summarize the content above.";
|
||||
|
||||
CompletionSKFunction summarize =
|
||||
kernel.getSemanticFunctionBuilder()
|
||||
.withPromptTemplate(prompt)
|
||||
.withFunctionName("summarize")
|
||||
.withRequestSettings(
|
||||
SKBuilders.completionRequestSettings()
|
||||
.temperature(0.2)
|
||||
.topP(0.5)
|
||||
.maxTokens(2000)
|
||||
.frequencyPenalty(0)
|
||||
.presencePenalty(0)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
String text =
|
||||
"""
|
||||
Demo (ancient Greek poet)
|
||||
From Wikipedia, the free encyclopedia
|
||||
Demo or Damo (Greek: Δεμώ, Δαμώ; fl. c. AD 200) was a Greek woman of the Roman period, known for a single epigram, engraved upon the Colossus of Memnon, which bears her name. She speaks of herself therein as a lyric poetess dedicated to the Muses, but nothing is known of her life.[1]
|
||||
Identity
|
||||
Demo was evidently Greek, as her name, a traditional epithet of Demeter, signifies. The name was relatively common in the Hellenistic world, in Egypt and elsewhere, and she cannot be further identified. The date of her visit to the Colossus of Memnon cannot be established with certainty, but internal evidence on the left leg suggests her poem was inscribed there at some point in or after AD 196.[2]
|
||||
Epigram
|
||||
There are a number of graffiti inscriptions on the Colossus of Memnon. Following three epigrams by Julia Balbilla, a fourth epigram, in elegiac couplets, entitled and presumably authored by "Demo" or "Damo" (the Greek inscription is difficult to read), is a dedication to the Muses.[2] The poem is traditionally published with the works of Balbilla, though the internal evidence suggests a different author.[1]
|
||||
In the poem, Demo explains that Memnon has shown her special respect. In return, Demo offers the gift for poetry, as a gift to the hero. At the end of this epigram, she addresses Memnon, highlighting his divine status by recalling his strength and holiness.[2]
|
||||
Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic dialect. The language indicates she was knowledgeable in Homeric poetry—'bearing a pleasant gift', for example, alludes to the use of that phrase throughout the Iliad and Odyssey.[a][2]
|
||||
""";
|
||||
|
||||
Mono<SKContext> result = summarize.invokeAsync(text);
|
||||
|
||||
LOGGER.info("Result: " + result.block().getResult());
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.e2e;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion;
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import com.microsoft.semantickernel.textcompletion.TextCompletion;
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class KernelTest extends AbstractKernelTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KernelTest.class);
|
||||
|
||||
private static void executeCompletion(Kernel kernel) {
|
||||
CompletionSKFunction summarize =
|
||||
kernel.getSemanticFunctionBuilder()
|
||||
.withPromptTemplate(
|
||||
"""
|
||||
{{$input}}
|
||||
|
||||
One line TLDR with the fewest words.""")
|
||||
.withRequestSettings(
|
||||
SKBuilders.completionRequestSettings()
|
||||
.temperature(0)
|
||||
.topP(0)
|
||||
.maxTokens(256)
|
||||
.frequencyPenalty(0)
|
||||
.presencePenalty(0)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
String text1 =
|
||||
"""
|
||||
1st Law of Thermodynamics - Energy cannot be created or destroyed.
|
||||
2nd Law of Thermodynamics - For a spontaneous process, the entropy of the universe increases.
|
||||
3rd Law of Thermodynamics - A perfect crystal at zero Kelvin has zero entropy""";
|
||||
|
||||
Mono<SKContext> mono = summarize.invokeAsync(text1);
|
||||
SKContext result = mono.block();
|
||||
|
||||
if (result != null) {
|
||||
LOGGER.info("Result: " + result.getResult());
|
||||
}
|
||||
}
|
||||
|
||||
public static Kernel buildKernel(OpenAIAsyncClient client, String model) {
|
||||
TextCompletion textCompletion = new OpenAITextCompletion(client, model);
|
||||
|
||||
return SKBuilders.kernel().withDefaultAIService(textCompletion).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public void endToEndTextGenerationTestAzureOpenAI() throws ConfigurationException {
|
||||
Kernel kernel = buildKernel(getOpenAIClient(), "text-davinci-003");
|
||||
executeCompletion(kernel);
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public void endToEndTextGenerationTestOpenAI() throws ConfigurationException, IOException {
|
||||
Kernel kernel = buildKernel(getOpenAIClient(), getOpenAIModel());
|
||||
executeCompletion(kernel);
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.e2e;
|
||||
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.planner.actionplanner.Plan;
|
||||
import com.microsoft.semantickernel.planner.sequentialplanner.SequentialPlanner;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class PlannerTest extends AbstractKernelTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PlannerTest.class);
|
||||
|
||||
@Test
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public void executeInlineFunction() throws ConfigurationException {
|
||||
Kernel kernel = buildTextCompletionKernel();
|
||||
kernel.importSkillFromDirectory(
|
||||
"SummarizeSkill", "../../../samples/skills", "SummarizeSkill");
|
||||
kernel.importSkillFromDirectory("WriterSkill", "../../../samples/skills", "WriterSkill");
|
||||
|
||||
SequentialPlanner planner = new SequentialPlanner(kernel, null, null);
|
||||
|
||||
Plan plan =
|
||||
planner.createPlanAsync(
|
||||
"Write a poem about John Doe, then translate it into Italian.")
|
||||
.block();
|
||||
|
||||
SKContext result = plan.invokeAsync().block();
|
||||
|
||||
LOGGER.info("Result: " + result.getResult());
|
||||
}
|
||||
}
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.e2e;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.connectors.ai.openai.textembeddings.OpenAITextEmbeddingGeneration;
|
||||
import com.microsoft.semantickernel.coreskills.TextMemorySkill;
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import com.microsoft.semantickernel.memory.MemoryQueryResult;
|
||||
import com.microsoft.semantickernel.memory.SemanticTextMemory;
|
||||
import com.microsoft.semantickernel.memory.VolatileMemoryStore;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class TextEmbeddingsTest extends AbstractKernelTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TextEmbeddingsTest.class);
|
||||
private static final int EXPECTED_EMBEDDING_SIZE = 1536;
|
||||
|
||||
@Test
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public void testEmbeddingGenerationOpenAI() throws ConfigurationException {
|
||||
OpenAIAsyncClient client = getOpenAIClient();
|
||||
String model = "text-embedding-ada-002";
|
||||
OpenAITextEmbeddingGeneration embeddingGeneration =
|
||||
new OpenAITextEmbeddingGeneration(client, model);
|
||||
|
||||
List<String> data = new ArrayList<>();
|
||||
data.add("This is just");
|
||||
// data.add("a test");
|
||||
|
||||
embeddingGeneration
|
||||
.generateEmbeddingsAsync(data)
|
||||
.block()
|
||||
.forEach(
|
||||
embedding ->
|
||||
Assertions.assertEquals(
|
||||
EXPECTED_EMBEDDING_SIZE, embedding.getVector().size()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public void testEmbeddingGeneration() throws ConfigurationException {
|
||||
String model = "text-embedding-ada-002";
|
||||
OpenAITextEmbeddingGeneration embeddingGeneration =
|
||||
new OpenAITextEmbeddingGeneration(getOpenAIClient(), model);
|
||||
|
||||
List<String> data = new ArrayList<>();
|
||||
data.add("This is just a test");
|
||||
|
||||
LOGGER.info(String.valueOf(embeddingGeneration.generateEmbeddingsAsync(data).block()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("isAzureTestEnabled")
|
||||
public void testMemory() throws ConfigurationException {
|
||||
|
||||
Kernel kernel = buildTextCompletionKernel();
|
||||
kernel.importSkill(new TextMemorySkill(), "aboutMe");
|
||||
|
||||
String skPrompt =
|
||||
"""
|
||||
|
||||
ChatBot can have a conversation with you about any topic.
|
||||
It can give explicit instructions or say 'I don't know' if it does not have an answer.
|
||||
|
||||
Information about me, from previous conversations:
|
||||
- {{$fact1}} {{recall $fact1}}
|
||||
- {{$fact2}} {{recall $fact2}}
|
||||
- {{$fact3}} {{recall $fact3}}
|
||||
- {{$fact4}} {{recall $fact4}}
|
||||
- {{$fact5}} {{recall $fact5}}
|
||||
|
||||
Chat:
|
||||
{{$history}}
|
||||
User: {{$userInput}}
|
||||
ChatBot:\s""";
|
||||
|
||||
CompletionSKFunction chat =
|
||||
kernel.getSemanticFunctionBuilder()
|
||||
.withPromptTemplate(skPrompt)
|
||||
.withFunctionName("recall")
|
||||
.withSkillName("aboutMe")
|
||||
.build();
|
||||
|
||||
VolatileMemoryStore volatileMemoryStore = new VolatileMemoryStore();
|
||||
volatileMemoryStore.createCollectionAsync("aboutMe").block();
|
||||
|
||||
OpenAITextEmbeddingGeneration embeddingGeneration =
|
||||
new OpenAITextEmbeddingGeneration(getOpenAIClient(), "text-embedding-ada-002");
|
||||
|
||||
SemanticTextMemory memory =
|
||||
SKBuilders.semanticTextMemory()
|
||||
.withEmbeddingGenerator(embeddingGeneration)
|
||||
.withStorage(volatileMemoryStore)
|
||||
.build();
|
||||
|
||||
SKContext context =
|
||||
SKBuilders.context()
|
||||
.withVariables(SKBuilders.variables().build())
|
||||
.withMemory(memory)
|
||||
.withSkills(kernel.getSkills())
|
||||
.build();
|
||||
|
||||
context.getSemanticMemory()
|
||||
.saveInformationAsync("aboutMe", "My name is Andrea", "fact1", null, null)
|
||||
.block();
|
||||
|
||||
context.getSemanticMemory()
|
||||
.saveInformationAsync(
|
||||
"aboutMe", "I currently work as a tour guide", "fact2", null, null)
|
||||
.block();
|
||||
|
||||
context.getSemanticMemory()
|
||||
.saveInformationAsync(
|
||||
"aboutMe", "I've been living in Seattle since 2005", "fact3", null, null)
|
||||
.block();
|
||||
|
||||
context.getSemanticMemory()
|
||||
.saveInformationAsync(
|
||||
"aboutMe",
|
||||
"I visited France and Italy five times since 2015",
|
||||
"fact4",
|
||||
null,
|
||||
null)
|
||||
.block();
|
||||
|
||||
context.getSemanticMemory()
|
||||
.saveInformationAsync("aboutMe", "My family is from New York", "fact5", null, null)
|
||||
.block();
|
||||
|
||||
String query = "Where are you from originally?";
|
||||
List<MemoryQueryResult> results =
|
||||
memory.searchAsync("aboutMe", query, 10, .5f, false).block();
|
||||
results.forEach(
|
||||
result ->
|
||||
System.out.printf(
|
||||
"%s %s (relevance=%f)%n",
|
||||
query, result.getMetadata().getText(), result.getRelevance()));
|
||||
}
|
||||
}
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.sequentialplanner;
|
||||
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.planner.actionplanner.Plan;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class SequentialPlanTest {
|
||||
|
||||
public static class Skills {
|
||||
@DefineSKFunction(name = "Skill1")
|
||||
public String skill1(@SKFunctionInputAttribute(description = "an input") String input) {
|
||||
return input + "::Skill1";
|
||||
}
|
||||
|
||||
@DefineSKFunction(name = "Skill2")
|
||||
public String skill2(@SKFunctionInputAttribute(description = "an input") String input) {
|
||||
return input + "::Skill2";
|
||||
}
|
||||
|
||||
@DefineSKFunction(name = "Skill3")
|
||||
public String skill3(
|
||||
@SKFunctionInputAttribute(description = "an input") String input,
|
||||
@SKFunctionParameters(
|
||||
description = "a variable to set the result to",
|
||||
required = true,
|
||||
name = "secondInput")
|
||||
String secondInput) {
|
||||
return input + "::" + secondInput + "::Skill3";
|
||||
}
|
||||
|
||||
@DefineSKFunction(name = "Skill4")
|
||||
public String skill4(
|
||||
@SKFunctionParameters(
|
||||
description = "a variable to set the result to",
|
||||
required = true,
|
||||
name = "anarg")
|
||||
String anarg) {
|
||||
return anarg + "::Skill4";
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withDirectInput() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill1 input="John Doe"/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
|
||||
SKContext result = makePlan(planString).invokeAsync("Foo").block();
|
||||
Assertions.assertEquals("John Doe::Skill1", result.getResult());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withNoInput() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill1/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
|
||||
SKContext result = makePlan(planString).invokeAsync("Foo").block();
|
||||
Assertions.assertEquals("Foo::Skill1", result.getResult());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withChainedInput() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill1/>
|
||||
<function.Skills.Skill2/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
|
||||
SKContext result = makePlan(planString).invokeAsync("Foo").block();
|
||||
Assertions.assertEquals("Foo::Skill1::Skill2", result.getResult());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void settingAVariable() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill1 setContextVariable="A"/>
|
||||
<function.Skills.Skill2/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
|
||||
SKContext result = makePlan(planString).invokeAsync("Foo").block();
|
||||
Assertions.assertEquals("Foo::Skill2", result.getResult());
|
||||
Assertions.assertEquals("Foo::Skill1", result.getVariables().get("A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usingAVariable() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill1 setContextVariable="A"/>
|
||||
<function.Skills.Skill2 input="$A"/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
|
||||
SKContext result = makePlan(planString).invokeAsync("Foo").block();
|
||||
Assertions.assertEquals("Foo::Skill1::Skill2", result.getResult());
|
||||
Assertions.assertEquals("Foo::Skill1", result.getVariables().get("A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withASecondArg() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill1 setContextVariable="A"/>
|
||||
<function.Skills.Skill2 input="$A"/>
|
||||
<function.Skills.Skill3 secondInput="x"/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
|
||||
SKContext result = makePlan(planString).invokeAsync("Foo").block();
|
||||
Assertions.assertEquals("Foo::Skill1::Skill2::x::Skill3", result.getResult());
|
||||
Assertions.assertEquals("Foo::Skill1", result.getVariables().get("A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noInput() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill1 setContextVariable="A"/>
|
||||
<function.Skills.Skill2 input="$A"/>
|
||||
<function.Skills.Skill3 secondInput="x"/>
|
||||
<function.Skills.Skill4 anarg="$A"/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
|
||||
SKContext result = makePlan(planString).invokeAsync("Foo").block();
|
||||
Assertions.assertEquals("Foo::Skill1::Skill4", result.getResult());
|
||||
Assertions.assertEquals("Foo::Skill1", result.getVariables().get("A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noInput2() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill4 anarg="foo"/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
|
||||
SKContext result = makePlan(planString).invokeAsync("Foo").block();
|
||||
Assertions.assertEquals("foo::Skill4", result.getResult());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noInput3() {
|
||||
String planString =
|
||||
"""
|
||||
<plan>
|
||||
<function.Skills.Skill4/>
|
||||
</plan><!-- END -->
|
||||
""";
|
||||
SKContext context = SKBuilders.context().build().setVariable("anarg", "foo");
|
||||
SKContext result = makePlan(planString).invokeAsync(context).block();
|
||||
Assertions.assertEquals("foo::Skill4", result.getResult());
|
||||
}
|
||||
|
||||
private static Plan makePlan(String planString) {
|
||||
Kernel kernel = SKBuilders.kernel().build();
|
||||
kernel.importSkill(new Skills(), "Skills");
|
||||
|
||||
return SequentialPlanParser.toPlanFromXml(
|
||||
planString.stripIndent(), "", kernel.getSkills(), kernel::getService);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
|
||||
import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class Example01NativeFunctionsTest {
|
||||
|
||||
private static class TextPlugin {
|
||||
|
||||
@DefineKernelFunction(description = "Change all string chars to uppercase.", name = "Uppercase")
|
||||
public String uppercase(
|
||||
@KernelFunctionParameter(description = "Text to uppercase", name = "input") String text) {
|
||||
return text.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void run() {
|
||||
// Load native plugin
|
||||
TextPlugin text = new TextPlugin();
|
||||
|
||||
// Use function without kernel
|
||||
String result = text.uppercase("ciao!");
|
||||
|
||||
Assertions.assertEquals("CIAO!", result);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.orchestration.FunctionResult;
|
||||
import com.microsoft.semantickernel.plugin.KernelPlugin;
|
||||
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example03_Arguments.StaticTextPlugin;
|
||||
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Demonstrates running a pipeline (a sequence of functions) on a
|
||||
* {@code com.microsoft.semantickernel.orchestration.SKContext}
|
||||
*/
|
||||
public class Example03_ArgumentsTest {
|
||||
|
||||
@Test
|
||||
public void main() {
|
||||
Kernel kernel = Kernel.builder().build();
|
||||
|
||||
// Load native plugin
|
||||
KernelPlugin functionCollection = KernelPluginFactory
|
||||
.createFromObject(new StaticTextPlugin(), "text");
|
||||
|
||||
KernelFunctionArguments arguments = KernelFunctionArguments.builder()
|
||||
.withInput("Today is: ")
|
||||
.withVariable("day", "Monday")
|
||||
.build();
|
||||
|
||||
FunctionResult<String> resultValue = kernel.invokeAsync(
|
||||
functionCollection.<String>get("AppendDay"))
|
||||
.withArguments(arguments)
|
||||
.block();
|
||||
|
||||
Assertions.assertEquals("Today is: Monday", resultValue.getResult());
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
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.KernelFunctionFromPrompt;
|
||||
import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@WireMockTest
|
||||
public class Example05_InlineFunctionDefinitionTest {
|
||||
|
||||
@Test
|
||||
public void main(WireMockRuntimeInfo wmRuntimeInfo) {
|
||||
final OpenAIAsyncClient client = new OpenAIClientBuilder()
|
||||
.endpoint("http://localhost:" + wmRuntimeInfo.getHttpPort())
|
||||
.buildAsyncClient();
|
||||
|
||||
TextGenerationService textGenerationService = TextGenerationService.builder()
|
||||
.withOpenAIAsyncClient(client)
|
||||
.withModelId("text-davinci-003")
|
||||
.build();
|
||||
|
||||
Kernel kernel = Kernel.builder()
|
||||
.withAIService(TextGenerationService.class, textGenerationService)
|
||||
.build();
|
||||
|
||||
System.out.println("======== Inline Function Definition ========");
|
||||
|
||||
// Function defined using few-shot design pattern
|
||||
String promptTemplate = """
|
||||
Generate a creative reason or excuse for the given event.
|
||||
Be creative and be funny. Let your imagination run wild.
|
||||
|
||||
Event: I am running late.
|
||||
Excuse: I was being held ransom by giraffe gangsters.
|
||||
|
||||
Event: I haven't been to the gym for a year
|
||||
Excuse: I've been too busy training my pet dragon.
|
||||
|
||||
Event: {{$input}}
|
||||
""".stripIndent();
|
||||
|
||||
var excuseFunction = new KernelFunctionFromPrompt.Builder<String>()
|
||||
.withName("Excuse")
|
||||
.withTemplate(promptTemplate)
|
||||
.withDefaultExecutionSettings(
|
||||
new PromptExecutionSettings.Builder()
|
||||
.withTemperature(0.4)
|
||||
.withTopP(1)
|
||||
.withMaxTokens(100)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
WireMockUtil.mockCompletionResponse("I missed the F1 final race", "a-response");
|
||||
|
||||
var result = kernel.invokeAsync(excuseFunction)
|
||||
.withArguments(
|
||||
KernelFunctionArguments.builder()
|
||||
.withInput("I missed the F1 final race")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertEquals("a-response", result.getResult());
|
||||
|
||||
WireMockUtil.mockCompletionResponse("sorry I forgot your birthday", "a-response-2");
|
||||
|
||||
result = kernel.invokeAsync(excuseFunction)
|
||||
.withArguments(
|
||||
KernelFunctionArguments.builder()
|
||||
.withInput("sorry I forgot your birthday")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertEquals("a-response-2", result.getResult());
|
||||
|
||||
WireMockUtil.mockCompletionResponse("Translate this date ", "a-response-3");
|
||||
|
||||
var date = DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneOffset.UTC)
|
||||
.format(Instant.ofEpochSecond(1));
|
||||
var message = "Translate this date " + date + " to French format";
|
||||
var fixedFunction = KernelFunction.<String>createFromPrompt(message)
|
||||
.withDefaultExecutionSettings(
|
||||
PromptExecutionSettings.builder()
|
||||
.withMaxTokens(100)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
FunctionResult<String> fixedFunctionResult = kernel
|
||||
.invokeAsync(fixedFunction)
|
||||
.block();
|
||||
|
||||
Assertions.assertEquals("a-response-3", fixedFunctionResult.getResult());
|
||||
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.plugin.KernelPlugin;
|
||||
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ImportingMultiplePluginsTest {
|
||||
|
||||
@Test
|
||||
public void canImportMultiplePlugins() {
|
||||
KernelPlugin summarize = KernelPluginFactory.importPluginFromResourcesDirectory(
|
||||
"Plugins",
|
||||
"SummarizePlugin",
|
||||
"Summarize",
|
||||
null,
|
||||
String.class);
|
||||
|
||||
KernelPlugin topics = KernelPluginFactory.importPluginFromResourcesDirectory(
|
||||
"Plugins",
|
||||
"SummarizePlugin",
|
||||
"Topics",
|
||||
null,
|
||||
String.class);
|
||||
|
||||
KernelPlugin notegen = KernelPluginFactory.importPluginFromResourcesDirectory(
|
||||
"Plugins",
|
||||
"SummarizePlugin",
|
||||
"Notegen",
|
||||
null,
|
||||
String.class);
|
||||
|
||||
Kernel kernel = Kernel.builder()
|
||||
.withPlugin(summarize)
|
||||
.withPlugin(topics)
|
||||
.withPlugin(notegen)
|
||||
.build();
|
||||
|
||||
Assertions.assertEquals(3,
|
||||
kernel.getPlugin("SummarizePlugin").getFunctions().size());
|
||||
Assertions.assertEquals("Summarize",
|
||||
kernel.getPlugin("SummarizePlugin").get("Summarize").getName());
|
||||
Assertions.assertEquals("Topics",
|
||||
kernel.getPlugin("SummarizePlugin").get("Topics").getName());
|
||||
Assertions.assertEquals("Notegen",
|
||||
kernel.getPlugin("SummarizePlugin").get("Notegen").getName());
|
||||
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.Kernel.Builder;
|
||||
import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion;
|
||||
import com.microsoft.semantickernel.hooks.KernelHook.FunctionInvokedHook;
|
||||
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.KernelFunctionFromPrompt;
|
||||
import com.microsoft.semantickernel.semanticfunctions.OutputVariable;
|
||||
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@WireMockTest
|
||||
public class KernelHooksTest {
|
||||
|
||||
private static Builder getKernelBuilder(WireMockRuntimeInfo wmRuntimeInfo) {
|
||||
final OpenAIAsyncClient client = new OpenAIClientBuilder()
|
||||
.endpoint("http://localhost:" + wmRuntimeInfo.getHttpPort())
|
||||
.buildAsyncClient();
|
||||
|
||||
ChatCompletionService openAIChatCompletion = OpenAIChatCompletion.builder()
|
||||
.withModelId("text-davinci-003")
|
||||
.withOpenAIAsyncClient(client)
|
||||
.build();
|
||||
|
||||
return Kernel.builder()
|
||||
.withAIService(ChatCompletionService.class, openAIChatCompletion);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUsageAsync(WireMockRuntimeInfo wmRuntimeInfo) {
|
||||
WireMockUtil.mockChatCompletionResponse("Write a random paragraph about", "a-response");
|
||||
Kernel kernel = getKernelBuilder(wmRuntimeInfo).build();
|
||||
|
||||
System.out.println("\n======== Get Usage Data ========\n");
|
||||
|
||||
// Initialize prompt
|
||||
String functionPrompt = "Write a random paragraph about: {{$input}}.";
|
||||
|
||||
KernelFunction<String> excuseFunction = KernelFunctionFromPrompt.builder()
|
||||
.withTemplate(functionPrompt)
|
||||
.withName("Excuse")
|
||||
.withDefaultExecutionSettings(PromptExecutionSettings
|
||||
.builder()
|
||||
.withMaxTokens(100)
|
||||
.withTemperature(0.4)
|
||||
.withTopP(1)
|
||||
.build())
|
||||
.withOutputVariable(new OutputVariable<>("result", String.class))
|
||||
.build();
|
||||
|
||||
AtomicBoolean preHookTriggered = new AtomicBoolean(false);
|
||||
|
||||
FunctionInvokingHook preHook = event -> {
|
||||
preHookTriggered.set(true);
|
||||
return event;
|
||||
};
|
||||
|
||||
AtomicBoolean removedPreExecutionHandlerTriggered = new AtomicBoolean(false);
|
||||
|
||||
FunctionInvokingHook removedPreExecutionHandler = event -> {
|
||||
removedPreExecutionHandlerTriggered.set(true);
|
||||
return event;
|
||||
};
|
||||
|
||||
AtomicBoolean postExecutionHandlerTriggered = new AtomicBoolean(false);
|
||||
|
||||
FunctionInvokedHook postExecutionHandler = event -> {
|
||||
postExecutionHandlerTriggered.set(true);
|
||||
return event;
|
||||
};
|
||||
|
||||
kernel.getGlobalKernelHooks().addHook(preHook);
|
||||
|
||||
// Demonstrate pattern for removing a handler.
|
||||
kernel.getGlobalKernelHooks().addHook("pre-invoke-removed", removedPreExecutionHandler);
|
||||
kernel.getGlobalKernelHooks().removeHook("pre-invoke-removed");
|
||||
kernel.getGlobalKernelHooks().addHook(postExecutionHandler);
|
||||
|
||||
kernel.invokeAsync(
|
||||
excuseFunction)
|
||||
.withArguments(
|
||||
KernelFunctionArguments
|
||||
.builder()
|
||||
.withVariable("input", "I missed the F1 final race")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertTrue(preHookTriggered.get());
|
||||
Assertions.assertFalse(removedPreExecutionHandlerTriggered.get());
|
||||
Assertions.assertTrue(postExecutionHandlerTriggered.get());
|
||||
}
|
||||
}
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion;
|
||||
import com.microsoft.semantickernel.aiservices.openai.textcompletion.OpenAITextGenerationService;
|
||||
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
|
||||
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
|
||||
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
|
||||
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
|
||||
import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
|
||||
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
|
||||
import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@WireMockTest
|
||||
public class RenderingTest {
|
||||
|
||||
private WireMockServer wm;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
wm = new WireMockServer();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
wm.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void textSemanticKernelTemplateXml() {
|
||||
buildTextKernel()
|
||||
.invokeAsync(
|
||||
KernelFunction
|
||||
.createFromPrompt("""
|
||||
Value: {{$value}}
|
||||
""")
|
||||
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
|
||||
.build())
|
||||
.withArguments(KernelFunctionArguments
|
||||
.builder()
|
||||
.withVariable("value", "<message role=\"user\">\"hello world\"</message>")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
// The actual body will be escaped as its json
|
||||
Assertions.assertTrue(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString()
|
||||
.contains("<message role=\\\"user\\\">\\\"hello world\\\"</message>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void textSemanticKernelTemplate() {
|
||||
buildTextKernel()
|
||||
.invokeAsync(
|
||||
KernelFunction
|
||||
.createFromPrompt("""
|
||||
Value: {{$value}}
|
||||
""")
|
||||
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
|
||||
.build())
|
||||
.withArguments(KernelFunctionArguments
|
||||
.builder()
|
||||
.withVariable("value", "{{$ignore}}")
|
||||
.withVariable("ignore", "dont show")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertFalse(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
|
||||
Assertions.assertTrue(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{$ignore}}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void textHandleBarsTemplate() {
|
||||
buildTextKernel()
|
||||
.invokeAsync(
|
||||
KernelFunction
|
||||
.createFromPrompt("""
|
||||
Value: {{value}}
|
||||
""")
|
||||
.withTemplateFormat("handlebars")
|
||||
.build())
|
||||
.withArguments(KernelFunctionArguments
|
||||
.builder()
|
||||
.withVariable("value", "{{ignore}}")
|
||||
.withVariable("ignore", "dont show")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertFalse(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
|
||||
Assertions.assertTrue(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{ignore}}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatSemanticKernelTemplateXml() {
|
||||
buildChatKernel()
|
||||
.invokeAsync(
|
||||
KernelFunction
|
||||
.createFromPrompt("""
|
||||
Value: {{$value}}
|
||||
""")
|
||||
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
|
||||
.build())
|
||||
.withArguments(KernelFunctionArguments
|
||||
.builder()
|
||||
.withVariable("value", "<message role=\"user\">\"hello world\"</message>")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
// The actual body will be escaped as its json
|
||||
Assertions.assertTrue(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString()
|
||||
.contains("<message role=\\\"user\\\">\\\"hello world\\\"</message>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatSemanticKernelTemplate() {
|
||||
buildChatKernel()
|
||||
.invokeAsync(
|
||||
KernelFunction
|
||||
.createFromPrompt("""
|
||||
Value: {{$value}}
|
||||
""")
|
||||
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
|
||||
.build())
|
||||
.withArguments(KernelFunctionArguments
|
||||
.builder()
|
||||
.withVariable("value", "{{$ignore}}")
|
||||
.withVariable("ignore", "dont show")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertFalse(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
|
||||
Assertions.assertTrue(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{$ignore}}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatHandleBarsTemplate() {
|
||||
buildChatKernel()
|
||||
.invokeAsync(
|
||||
KernelFunction
|
||||
.createFromPrompt("""
|
||||
Value: {{value}}
|
||||
""")
|
||||
.withTemplateFormat("handlebars")
|
||||
.build())
|
||||
.withArguments(KernelFunctionArguments
|
||||
.builder()
|
||||
.withVariable("value", "{{ignore}}")
|
||||
.withVariable("ignore", "dont show")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertFalse(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
|
||||
Assertions.assertTrue(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{ignore}}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatSemanticKernelTemplate2() {
|
||||
buildChatKernel()
|
||||
.invokeAsync(
|
||||
KernelFunction
|
||||
.createFromPrompt("""
|
||||
<message role="user">Value: {{$value}}</message>
|
||||
""")
|
||||
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
|
||||
.build())
|
||||
.withArguments(KernelFunctionArguments
|
||||
.builder()
|
||||
.withVariable("value", "{{$ignore}}")
|
||||
.withVariable("ignore", "dont show")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertFalse(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
|
||||
Assertions.assertTrue(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{$ignore}}"));
|
||||
}
|
||||
|
||||
@DefineKernelFunction(name = "WithEmptyListReturn")
|
||||
public List<Integer> WithEmptyListReturn() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@DefineKernelFunction(name = "WithListReturn")
|
||||
public List<Integer> WithListReturn() {
|
||||
return List.of(1, 2, 3);
|
||||
}
|
||||
|
||||
@DefineKernelFunction(name = "WithListReturn2", returnType = "java.util.List")
|
||||
public List<Integer> WithListReturn2() {
|
||||
return List.of(1, 2, 3);
|
||||
}
|
||||
|
||||
@DefineKernelFunction(name = "WithListReturn3", returnType = "java.util.List")
|
||||
public Mono<List<Integer>> WithListReturn3() {
|
||||
return Mono.just(List.of(1, 2, 3));
|
||||
}
|
||||
|
||||
@DefineKernelFunction(name = "WithListReturn4", returnType = "java.util.List")
|
||||
public Mono<List<Integer>> WithListReturn4() {
|
||||
return Mono.just(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canHandleIterableReturnFromFunction() {
|
||||
buildChatKernel()
|
||||
.invokeAsync(
|
||||
KernelFunction
|
||||
.createFromPrompt("""
|
||||
<message role="user">{{RenderingTest-WithEmptyListReturn}}</message>
|
||||
<message role="user">{{RenderingTest-WithListReturn}}</message>
|
||||
<message role="user">{{RenderingTest-WithListReturn2}}</message>
|
||||
<message role="user">{{RenderingTest-WithListReturn3}}</message>
|
||||
<message role="user">{{RenderingTest-WithListReturn4}}</message>
|
||||
""")
|
||||
.withTemplateFormat("handlebars")
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertTrue(
|
||||
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().equals(
|
||||
"{\"messages\":["
|
||||
+ "{\"role\":\"user\",\"content\":\"[]\"},"
|
||||
+ "{\"role\":\"user\",\"content\":\"[1, 2, 3]\"},"
|
||||
+ "{\"role\":\"user\",\"content\":\"[1, 2, 3]\"},"
|
||||
+ "{\"role\":\"user\",\"content\":\"[1, 2, 3]\"},"
|
||||
+ "{\"role\":\"user\",\"content\":\"[]\"}"
|
||||
+ "],\"model\":\"gpt-35-turbo-2\"}"));
|
||||
}
|
||||
|
||||
private Kernel buildTextKernel() {
|
||||
wm.addStubMapping(
|
||||
stubFor(
|
||||
ToolCallBehaviourTest.buildTextResponse(" ", """
|
||||
"choices": [
|
||||
{
|
||||
"text": "Value: bar"
|
||||
}
|
||||
]
|
||||
""")));
|
||||
wm.start();
|
||||
|
||||
final OpenAIAsyncClient client = new OpenAIClientBuilder()
|
||||
.endpoint("http://localhost:" + wm.port() + "/")
|
||||
.buildAsyncClient();
|
||||
|
||||
TextGenerationService textGenerationService = OpenAITextGenerationService.builder()
|
||||
.withOpenAIAsyncClient(client)
|
||||
.withModelId("gpt-35-turbo-2")
|
||||
.build();
|
||||
|
||||
Kernel kernel = Kernel.builder()
|
||||
.withAIService(TextGenerationService.class, textGenerationService)
|
||||
.build();
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private Kernel buildChatKernel() {
|
||||
wm.addStubMapping(
|
||||
stubFor(
|
||||
ToolCallBehaviourTest.buildResponse(" ", """
|
||||
"choices" : [
|
||||
{
|
||||
"finish_reason" : "stop",
|
||||
"index" : 0,
|
||||
"message" : {
|
||||
"content" : "done",
|
||||
"role" : "assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
""")));
|
||||
wm.start();
|
||||
|
||||
final OpenAIAsyncClient client = new OpenAIClientBuilder()
|
||||
.endpoint("http://localhost:" + wm.port() + "/")
|
||||
.buildAsyncClient();
|
||||
|
||||
ChatCompletionService textGenerationService = OpenAIChatCompletion.builder()
|
||||
.withOpenAIAsyncClient(client)
|
||||
.withModelId("gpt-35-turbo-2")
|
||||
.build();
|
||||
|
||||
Kernel kernel = Kernel.builder()
|
||||
.withAIService(ChatCompletionService.class, textGenerationService)
|
||||
.withPlugin(KernelPluginFactory.createFromObject(this, "RenderingTest"))
|
||||
.build();
|
||||
return kernel;
|
||||
}
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.MappingBuilder;
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
||||
import com.github.tomakehurst.wiremock.matching.ContainsPattern;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatMessageContent;
|
||||
import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIFunctionToolCall;
|
||||
import com.microsoft.semantickernel.orchestration.InvocationContext;
|
||||
import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
|
||||
import com.microsoft.semantickernel.plugin.KernelPlugin;
|
||||
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
|
||||
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromMethod;
|
||||
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 java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
@WireMockTest
|
||||
public class ToolCallBehaviourTest {
|
||||
|
||||
private WireMockServer wm;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
wm = new WireMockServer();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
wm.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAutoInvokedIsNotCalled() throws NoSuchMethodException {
|
||||
ChatCompletionService chatCompletionService = getChatCompletionService();
|
||||
|
||||
TestPlugin testPlugin = Mockito.spy(new TestPlugin());
|
||||
KernelFunction<String> method = KernelFunctionFromMethod.<String>builder()
|
||||
.withFunctionName("doIt")
|
||||
.withMethod(TestPlugin.class.getMethod("doIt"))
|
||||
.withTarget(testPlugin)
|
||||
.withPluginName("apluginname")
|
||||
.build();
|
||||
|
||||
Kernel kernel = Kernel.builder()
|
||||
.withAIService(ChatCompletionService.class, chatCompletionService)
|
||||
.withPlugin(
|
||||
new KernelPlugin(
|
||||
"apluginname",
|
||||
"A plugin description",
|
||||
Map.of("doIt", method)))
|
||||
.build();
|
||||
|
||||
ChatHistory messages = new ChatHistory();
|
||||
messages.addMessage(
|
||||
new ChatMessageContent<>(
|
||||
AuthorRole.USER,
|
||||
"Call A function"));
|
||||
|
||||
List<ChatMessageContent<?>> result = chatCompletionService
|
||||
.getChatMessageContentsAsync(
|
||||
messages,
|
||||
kernel,
|
||||
InvocationContext.builder()
|
||||
.withToolCallBehavior(
|
||||
ToolCallBehavior.allowOnlyKernelFunctions(false, method))
|
||||
.build())
|
||||
.block();
|
||||
|
||||
List<OpenAIFunctionToolCall> toolCalls = ((OpenAIChatMessageContent<?>) result.get(0))
|
||||
.getToolCall();
|
||||
|
||||
Assertions.assertNotNull(toolCalls);
|
||||
Assertions.assertEquals(1, toolCalls.size());
|
||||
Assertions.assertEquals("apluginname", toolCalls.get(0).getPluginName());
|
||||
Assertions.assertEquals("doIt", toolCalls.get(0).getFunctionName());
|
||||
Assertions.assertEquals("call_abc123", toolCalls.get(0).getId());
|
||||
|
||||
Mockito.verify(testPlugin, Mockito.times(0)).doIt();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toolIsInvoked() throws NoSuchMethodException {
|
||||
ChatCompletionService chatCompletionService = getChatCompletionService();
|
||||
|
||||
TestPlugin testPlugin = Mockito.spy(new TestPlugin());
|
||||
KernelFunction<String> method = KernelFunctionFromMethod.<String>builder()
|
||||
.withFunctionName("doIt")
|
||||
.withMethod(TestPlugin.class.getMethod("doIt"))
|
||||
.withTarget(testPlugin)
|
||||
.withPluginName("apluginname")
|
||||
.build();
|
||||
|
||||
Kernel kernel = Kernel.builder()
|
||||
.withAIService(ChatCompletionService.class, chatCompletionService)
|
||||
.withPlugin(
|
||||
new KernelPlugin(
|
||||
"apluginname",
|
||||
"A plugin description",
|
||||
Map.of("doIt", method)))
|
||||
.build();
|
||||
|
||||
ChatHistory messages = new ChatHistory();
|
||||
messages.addMessage(
|
||||
new ChatMessageContent<>(
|
||||
AuthorRole.USER,
|
||||
"Call A function"));
|
||||
|
||||
List<ChatMessageContent<?>> result = chatCompletionService
|
||||
.getChatMessageContentsAsync(
|
||||
messages,
|
||||
kernel,
|
||||
InvocationContext.builder()
|
||||
.withToolCallBehavior(
|
||||
ToolCallBehavior.allowAllKernelFunctions(true))
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertTrue(result.get(0).getContent().contains("tool call done"));
|
||||
Mockito.verify(testPlugin, Mockito.times(1)).doIt();
|
||||
|
||||
result = chatCompletionService
|
||||
.getChatMessageContentsAsync(
|
||||
messages,
|
||||
kernel,
|
||||
InvocationContext.builder()
|
||||
.withToolCallBehavior(
|
||||
ToolCallBehavior.allowOnlyKernelFunctions(true, Arrays.asList(method)))
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertTrue(result.get(0).getContent().contains("tool call done"));
|
||||
Mockito.verify(testPlugin, Mockito.times(2)).doIt();
|
||||
|
||||
result = chatCompletionService
|
||||
.getChatMessageContentsAsync(
|
||||
messages,
|
||||
kernel,
|
||||
InvocationContext.builder()
|
||||
.withToolCallBehavior(
|
||||
ToolCallBehavior.allowOnlyKernelFunctions(true, method))
|
||||
.build())
|
||||
.block();
|
||||
|
||||
Assertions.assertTrue(result.get(0).getContent().contains("tool call done"));
|
||||
Mockito.verify(testPlugin, Mockito.times(3)).doIt();
|
||||
}
|
||||
|
||||
private ChatCompletionService getChatCompletionService() {
|
||||
wm
|
||||
.addStubMapping(
|
||||
stubFor(
|
||||
buildResponse("Call A function", """
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "apluginname-doIt",
|
||||
"arguments": "{}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "tool_calls"
|
||||
}
|
||||
]
|
||||
""")));
|
||||
|
||||
wm.addStubMapping(
|
||||
stubFor(
|
||||
buildResponse("Tool call performed", """
|
||||
"choices" : [
|
||||
{
|
||||
"finish_reason" : "stop",
|
||||
"index" : 0,
|
||||
"message" : {
|
||||
"content" : "tool call done",
|
||||
"role" : "assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
""")));
|
||||
|
||||
wm.start();
|
||||
|
||||
final OpenAIAsyncClient client = new OpenAIClientBuilder()
|
||||
.endpoint("http://localhost:" + wm.port() + "/")
|
||||
.buildAsyncClient();
|
||||
|
||||
return ChatCompletionService.builder()
|
||||
.withOpenAIAsyncClient(client)
|
||||
.withModelId("gpt-35-turbo-2")
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MappingBuilder buildTextResponse(String bodyMatcher, String responseBody) {
|
||||
return post(urlEqualTo(
|
||||
"//openai/deployments/gpt-35-turbo-2/completions?api-version=2024-02-15-preview"))
|
||||
.withRequestBody(new ContainsPattern(bodyMatcher))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody(formTextResponse(responseBody)));
|
||||
}
|
||||
|
||||
public static MappingBuilder buildResponse(String bodyMatcher, String responseBody) {
|
||||
return post(urlEqualTo(
|
||||
"//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2024-02-15-preview"))
|
||||
.withRequestBody(new ContainsPattern(bodyMatcher))
|
||||
.willReturn(
|
||||
aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody(formResponse(responseBody)));
|
||||
}
|
||||
|
||||
public static class TestPlugin {
|
||||
|
||||
public String doIt() {
|
||||
return "Tool call performed";
|
||||
}
|
||||
}
|
||||
|
||||
private static String formTextResponse(String chatChoices) {
|
||||
return """
|
||||
{
|
||||
"id": "cmpl-3QJ9z1J9z1J9z1J9z1J9z1J9",
|
||||
"usage": {
|
||||
"total_tokens": 4,
|
||||
"completion_tokens": 1,
|
||||
"prompt_tokens": 1
|
||||
},
|
||||
%s
|
||||
}
|
||||
""".formatted(chatChoices);
|
||||
}
|
||||
|
||||
private static String formResponse(String chatChoices) {
|
||||
return """
|
||||
{
|
||||
%s,
|
||||
"created" : 1707253061,
|
||||
"id" : "chatcmpl-xxx",
|
||||
"model" : "gpt-35-turbo",
|
||||
"object" : "chat.completion",
|
||||
"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" : 67,
|
||||
"prompt_tokens" : 17,
|
||||
"total_tokens" : 84
|
||||
}
|
||||
}
|
||||
""".formatted(chatChoices);
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import com.github.tomakehurst.wiremock.matching.RegexPattern;
|
||||
import com.github.tomakehurst.wiremock.matching.UrlPathPattern;
|
||||
|
||||
public class WireMockUtil {
|
||||
|
||||
public static void mockCompletionResponse(
|
||||
String regexMatcher,
|
||||
String response) {
|
||||
String body = """
|
||||
{
|
||||
"id": "1",
|
||||
"object": "text_completion",
|
||||
"created": 1589478378,
|
||||
"model": "text-davinci-003",
|
||||
"system_fingerprint": "fp_44709d6fb",
|
||||
"choices": [
|
||||
{
|
||||
"text": "%s",
|
||||
"index": 0,
|
||||
"logprobs": null,
|
||||
"finish_reason": "length"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 2,
|
||||
"total_tokens": 3
|
||||
}
|
||||
}
|
||||
"""
|
||||
.formatted(response);
|
||||
|
||||
WireMock.reset();
|
||||
WireMock.stubFor(WireMock
|
||||
.post(new UrlPathPattern(
|
||||
new RegexPattern("/openai/deployments/text-davinci-003/completions"), true))
|
||||
.withRequestBody(WireMock.matching(".*" + regexMatcher + ".*"))
|
||||
.willReturn(WireMock.ok()
|
||||
.withBody(body)));
|
||||
}
|
||||
|
||||
public static void mockChatCompletionResponse(
|
||||
String regexMatcher,
|
||||
String response) {
|
||||
String body = """
|
||||
{
|
||||
"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" : "length",
|
||||
"index" : 0,
|
||||
"message" : {
|
||||
"content" : "%s",
|
||||
"role" : "assistant"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created" : 1707253019,
|
||||
"id" : "chatcmpl-xxx",
|
||||
"model" : "gpt-35-turbo",
|
||||
"object" : "chat.completion",
|
||||
"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" : 256,
|
||||
"prompt_tokens" : 69,
|
||||
"total_tokens" : 325
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
.formatted(response);
|
||||
|
||||
WireMock.reset();
|
||||
WireMock.stubFor(WireMock
|
||||
.post(new UrlPathPattern(
|
||||
new RegexPattern("/openai/deployments/text-davinci-003/chat/completions"), true))
|
||||
.withRequestBody(WireMock.matching(".*" + regexMatcher + ".*"))
|
||||
.willReturn(WireMock.ok()
|
||||
.withBody(body)));
|
||||
}
|
||||
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example01_NativeFunctions;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example03_Arguments;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example05_InlineFunctionDefinition;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example06_TemplateLanguage;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example09_FunctionTypes;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example10_DescribeAllPluginsAndFunctions;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example13_ConversationSummaryPlugin;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example17_ChatGPT;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example27_PromptFunctionsUsingChatGPT;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example33_Chat;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example41_HttpClientUsage;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example43_GetModelResult;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example44_MultiChatCompletion;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example49_LogitBias;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example55_TextChunker;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example56_TemplateMethodFunctionsWithMultipleArguments;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example57_KernelHooks;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example58_ConfigureExecutionSettings;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example60_AdvancedMethodFunctions;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example61_MultipleLLMs;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example62_CustomAIServiceSelector;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example63_ChatCompletionPrompts;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example64_MultiplePromptTemplates;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.Example69_MutableKernelPlugin;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.java.FunctionsWithinPrompts_Example;
|
||||
import com.microsoft.semantickernel.samples.syntaxexamples.java.KernelFunctionYaml_Example;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class WiremockExamplesIT {
|
||||
|
||||
public static List<Class<?>> mains = Arrays.asList(
|
||||
FunctionsWithinPrompts_Example.class,
|
||||
KernelFunctionYaml_Example.class,
|
||||
Example01_NativeFunctions.class,
|
||||
Example03_Arguments.class,
|
||||
Example05_InlineFunctionDefinition.class,
|
||||
Example06_TemplateLanguage.class,
|
||||
//Example08_RetryHandler.class,
|
||||
Example09_FunctionTypes.class,
|
||||
Example10_DescribeAllPluginsAndFunctions.class,
|
||||
//Example11_WebSearchQueries.class,
|
||||
Example13_ConversationSummaryPlugin.class,
|
||||
Example17_ChatGPT.class,
|
||||
//Example26_AADAuth.class,
|
||||
|
||||
Example27_PromptFunctionsUsingChatGPT.class,
|
||||
|
||||
// Difficulty with time causing wiremock to fail
|
||||
//Example30_ChatWithPrompts.class,
|
||||
Example33_Chat.class,
|
||||
Example41_HttpClientUsage.class,
|
||||
Example43_GetModelResult.class,
|
||||
Example44_MultiChatCompletion.class,
|
||||
Example49_LogitBias.class,
|
||||
Example55_TextChunker.class,
|
||||
Example56_TemplateMethodFunctionsWithMultipleArguments.class,
|
||||
Example57_KernelHooks.class,
|
||||
Example58_ConfigureExecutionSettings.class,
|
||||
Example60_AdvancedMethodFunctions.class,
|
||||
Example61_MultipleLLMs.class,
|
||||
Example62_CustomAIServiceSelector.class,
|
||||
Example63_ChatCompletionPrompts.class,
|
||||
Example64_MultiplePromptTemplates.class,
|
||||
Example69_MutableKernelPlugin.class);
|
||||
|
||||
public static WireMockServer createWiremockServer(String dir) {
|
||||
return new WireMockServer(
|
||||
WireMockConfiguration.wireMockConfig()
|
||||
.httpsPort(8443)
|
||||
.trustStorePath("scripts/client.truststore")
|
||||
.trustStorePassword("password")
|
||||
.trustStoreType("jks")
|
||||
.keystorePath("scripts/server.keystore")
|
||||
.keystorePassword("password")
|
||||
.keystoreType("jks")
|
||||
.usingFilesUnderDirectory(dir));
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
mockInputStream();
|
||||
|
||||
new WiremockExamplesIT()
|
||||
.runSamplesTest()
|
||||
.forEach(it -> {
|
||||
try {
|
||||
it.getExecutable().execute();
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void mockInputStream() throws IOException {
|
||||
FunctionsWithinPrompts_Example.INPUT = Mockito.mock(InputStream.class);
|
||||
|
||||
Queue<String> messages = new ArrayDeque<>();
|
||||
|
||||
messages.addAll(Arrays.asList(
|
||||
"Can you draft me an email to the marketing team?",
|
||||
"Tell them to go ahead with the plan",
|
||||
"That is all"));
|
||||
|
||||
Mockito
|
||||
.when(FunctionsWithinPrompts_Example.INPUT.read(
|
||||
Mockito.any(byte[].class),
|
||||
Mockito.anyInt(),
|
||||
Mockito.anyInt()))
|
||||
.then(invocation -> {
|
||||
String message = messages.poll() + "\n";
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
System.arraycopy(message.getBytes(), 0, bytes, 0, message.getBytes().length);
|
||||
|
||||
return message.getBytes().length;
|
||||
});
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
public Stream<DynamicTest> runSamplesTest() {
|
||||
return mains
|
||||
.stream()
|
||||
.map(
|
||||
testClazz -> DynamicTest
|
||||
.dynamicTest(
|
||||
testClazz.getSimpleName() + "Test",
|
||||
() -> {
|
||||
mockInputStream();
|
||||
System.out.println("Running: " + testClazz.getSimpleName());
|
||||
WireMockServer server = null;
|
||||
try {
|
||||
Method main = testClazz.getMethod("main",
|
||||
String[].class);
|
||||
|
||||
server = createWiremockServer("src/test/resources/wiremock");
|
||||
|
||||
server.start();
|
||||
main.invoke(null, new Object[] { null });
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (server != null) {
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.tests;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.recording.RecordSpecBuilder;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
public class WiremockRecord {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
record(
|
||||
() -> {
|
||||
WiremockExamplesIT.mains.forEach(testClazz -> {
|
||||
try {
|
||||
Method main = testClazz.getMethod("main", String[].class);
|
||||
main.invoke(null, new Object[] { null });
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
args[0]);
|
||||
}
|
||||
|
||||
public static void compressRecordings(Path mappings) throws IOException {
|
||||
Files.newDirectoryStream(mappings)
|
||||
.forEach(path -> {
|
||||
try {
|
||||
JsonNode data = new ObjectMapper().readTree(path.toFile());
|
||||
|
||||
JsonNode body = new ObjectMapper().readTree(
|
||||
data.get("response").get("body").asText());
|
||||
|
||||
if (body.get("id").asText().contains("chatcmpl-")) {
|
||||
|
||||
((ObjectNode) body).put("id", "chatcmpl-xxx");
|
||||
|
||||
body
|
||||
.get("choices")
|
||||
.forEach(choice -> {
|
||||
((ObjectNode) choice.get("message")).put("content",
|
||||
UUID.randomUUID().toString());
|
||||
});
|
||||
|
||||
((ObjectNode) data.get("response")).put("body", body.toString());
|
||||
} else if (body.get("id").asText().contains("cmpl-")) {
|
||||
|
||||
((ObjectNode) body).put("id", "cmpl-xxx");
|
||||
|
||||
body
|
||||
.get("choices")
|
||||
.forEach(choice -> {
|
||||
((ObjectNode) choice).put("text", UUID.randomUUID().toString());
|
||||
});
|
||||
|
||||
((ObjectNode) data.get("response")).put("body", body.toString());
|
||||
}
|
||||
|
||||
Files.writeString(path, data.toPrettyString());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void record(Runnable run, String targetUrl) throws IOException {
|
||||
Path dir = Path.of("target", "wiremock");
|
||||
if (dir.toFile().exists()) {
|
||||
dir.toFile().delete();
|
||||
}
|
||||
|
||||
Files.createDirectories(dir.resolve("mappings"));
|
||||
|
||||
System.out.println("Recording to: " + dir.toFile().getCanonicalPath());
|
||||
|
||||
WireMockServer wireMockServer = WiremockExamplesIT.createWiremockServer(
|
||||
dir.toFile().getCanonicalPath() + "/");
|
||||
wireMockServer.start();
|
||||
wireMockServer.startRecording(
|
||||
new RecordSpecBuilder()
|
||||
.forTarget(targetUrl)
|
||||
.makeStubsPersistent(true)
|
||||
.build());
|
||||
|
||||
try {
|
||||
run.run();
|
||||
} finally {
|
||||
wireMockServer.stopRecording().getStubMappings();
|
||||
wireMockServer.stop();
|
||||
}
|
||||
|
||||
compressRecordings(dir.resolve("mappings"));
|
||||
|
||||
System.out.println("Saved output to: " + dir.toFile().getAbsolutePath());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<Root level="warn">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"priority": 1,
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2024-02-15-preview",
|
||||
"bodyPatterns": [
|
||||
{
|
||||
"contains": "That is all"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"body": "{\"id\":\"chatcmpl-xxx\",\"object\":\"chat.completion\",\"created\":1707253061,\"model\":\"gpt-35-turbo\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"EndConversation\"},\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"usage\":{\"prompt_tokens\":17,\"completion_tokens\":67,\"total_tokens\":84}}",
|
||||
"headers": {},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2024-02-15-preview"
|
||||
},
|
||||
"response": {
|
||||
"body": "{\"id\":\"chatcmpl-xxx\",\"object\":\"chat.completion\",\"created\":1707253061,\"model\":\"gpt-35-turbo\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"1979b4ce-5463-4cfb-8ec8-1d05c4b44ccf\"},\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"usage\":{\"prompt_tokens\":17,\"completion_tokens\":67,\"total_tokens\":84}}",
|
||||
"headers": {},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "//openai/deployments/gpt-35-turbo/chat/completions?api-version=2024-02-15-preview"
|
||||
},
|
||||
"response": {
|
||||
"body": "{\"id\":\"chatcmpl-xxx\",\"object\":\"chat.completion\",\"created\":1707253039,\"model\":\"gpt-35-turbo\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"ac9817bc-7e1a-48e4-b06c-0ff7618b88c6\"},\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"usage\":{\"prompt_tokens\":26,\"completion_tokens\":131,\"total_tokens\":157}}",
|
||||
"headers": {},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "//openai/deployments/text-davinci-003/completions?api-version=2024-02-15-preview"
|
||||
},
|
||||
"response": {
|
||||
"body": "{\"id\":\"cmpl-xxx\",\"object\":\"text_completion\",\"created\":1707253062,\"model\":\"text-davinci-003\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"choices\":[{\"text\":\"7949e981-c632-422f-9b76-335a2379cd83\",\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":26,\"total_tokens\":36}}",
|
||||
"headers": {},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<version>1.0.3-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<groupId>com.microsoft.semantic-kernel.test</groupId>
|
||||
<name>Semantic Kernel Api Tests</name>
|
||||
<artifactId>api-test</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<!--
|
||||
<module>api-usage-example</module>
|
||||
-->
|
||||
<module>integration-tests</module>
|
||||
</modules>
|
||||
|
||||
@@ -48,6 +50,13 @@
|
||||
<release>${maven.compiler.release}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-connectors-ai-openai</artifactId>
|
||||
|
||||
<name>Semantic Kernel OpenAI Connectors</name>
|
||||
<description>Defines OpenAI connectors to be used with the Semantic Kernel</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.azure</groupId>
|
||||
<artifactId>azure-identity</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.inject</groupId>
|
||||
<artifactId>jakarta.inject-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.azuresdk;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.microsoft.semantickernel.ai.AIException;
|
||||
|
||||
public abstract class ClientBase {
|
||||
private final String modelId;
|
||||
private final OpenAIAsyncClient client;
|
||||
|
||||
public ClientBase(OpenAIAsyncClient client, String modelId) {
|
||||
this.modelId = modelId;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
protected String getModelId() {
|
||||
return modelId;
|
||||
}
|
||||
|
||||
protected OpenAIAsyncClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
protected static void validateMaxTokens(int maxTokens) {
|
||||
if (maxTokens < 1) {
|
||||
throw new AIException(
|
||||
AIException.ErrorCodes.INVALID_REQUEST,
|
||||
"MaxTokens "
|
||||
+ maxTokens
|
||||
+ " is not valid, the value must be greater than zero");
|
||||
}
|
||||
}
|
||||
}
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.chatcompletion;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.models.ChatChoice;
|
||||
import com.azure.ai.openai.models.ChatCompletions;
|
||||
import com.azure.ai.openai.models.ChatCompletionsOptions;
|
||||
import com.azure.ai.openai.models.ChatMessage;
|
||||
import com.azure.ai.openai.models.ChatRole;
|
||||
import com.microsoft.semantickernel.Verify;
|
||||
import com.microsoft.semantickernel.ai.AIException;
|
||||
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
|
||||
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
|
||||
import com.microsoft.semantickernel.chatcompletion.ChatRequestSettings;
|
||||
import com.microsoft.semantickernel.connectors.ai.openai.azuresdk.ClientBase;
|
||||
import com.microsoft.semantickernel.exceptions.NotSupportedException;
|
||||
import com.microsoft.semantickernel.exceptions.NotSupportedException.ErrorCodes;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionRequestSettings;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionType;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/** OpenAI chat completion client. */
|
||||
public class OpenAIChatCompletion extends ClientBase implements ChatCompletion<OpenAIChatHistory> {
|
||||
|
||||
private CompletionType defaultCompletionType;
|
||||
|
||||
public OpenAIChatCompletion(
|
||||
OpenAIAsyncClient client, String modelId, CompletionType defaultCompletionType) {
|
||||
super(client, modelId);
|
||||
this.defaultCompletionType = defaultCompletionType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<String>> completeAsync(
|
||||
@Nonnull String text, @Nonnull CompletionRequestSettings requestSettings) {
|
||||
ChatRequestSettings chatRequestSettings = new ChatRequestSettings(requestSettings);
|
||||
return generateMessageAsync(internalCreateNewChat(null, text), chatRequestSettings)
|
||||
.map(Arrays::asList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> completeStreamAsync(
|
||||
@Nonnull String text, @Nonnull CompletionRequestSettings requestSettings) {
|
||||
ChatRequestSettings chatRequestSettings = new ChatRequestSettings(requestSettings);
|
||||
return generateMessageStream(internalCreateNewChat(null, text), chatRequestSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionType defaultCompletionType() {
|
||||
return defaultCompletionType;
|
||||
}
|
||||
|
||||
public static class Builder implements ChatCompletion.Builder<OpenAIChatHistory> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
@Nullable private OpenAIAsyncClient client;
|
||||
@Nullable private String modelId;
|
||||
private CompletionType defaultCompletionType = CompletionType.STREAMING;
|
||||
|
||||
public Builder withOpenAIClient(OpenAIAsyncClient client) {
|
||||
this.client = client;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withDefaultCompletionType(CompletionType completionType) {
|
||||
this.defaultCompletionType = completionType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withModelId(String modelId) {
|
||||
this.modelId = modelId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion build() {
|
||||
if (client == null) {
|
||||
throw new NotSupportedException(ErrorCodes.NOT_SUPPORTED, "OpenAI client not set");
|
||||
}
|
||||
if (modelId == null) {
|
||||
throw new NotSupportedException(ErrorCodes.NOT_SUPPORTED, "Model ID not set");
|
||||
}
|
||||
return new OpenAIChatCompletion(client, modelId, defaultCompletionType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new chat message
|
||||
*
|
||||
* @param chat Chat history
|
||||
* @param requestSettings AI request settings
|
||||
* @return The response generated by the request
|
||||
*/
|
||||
@Override
|
||||
public Mono<String> generateMessageAsync(
|
||||
OpenAIChatHistory chat, @Nullable ChatRequestSettings requestSettings) {
|
||||
|
||||
if (requestSettings == null) {
|
||||
requestSettings = new ChatRequestSettings();
|
||||
}
|
||||
return this.internalGenerateChatMessageAsync(chat, requestSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new chat message
|
||||
*
|
||||
* @param chat Chat history
|
||||
* @param requestSettings AI request settings
|
||||
* @return The response generated by the request
|
||||
*/
|
||||
private Mono<String> internalGenerateChatMessageAsync(
|
||||
ChatHistory chat, ChatRequestSettings requestSettings) {
|
||||
Verify.notNull(chat);
|
||||
Verify.notNull(requestSettings);
|
||||
|
||||
validateMaxTokens(requestSettings.getMaxTokens());
|
||||
ChatCompletionsOptions options = createChatCompletionsOptions(requestSettings, chat);
|
||||
|
||||
return getClient()
|
||||
.getChatCompletions(getModelId(), options)
|
||||
.flatMap(
|
||||
response -> {
|
||||
if (response == null || response.getChoices().isEmpty()) {
|
||||
return Mono.error(
|
||||
new AIException(
|
||||
AIException.ErrorCodes.INVALID_RESPONSE_CONTENT,
|
||||
"Chat completions not found"));
|
||||
} else {
|
||||
return Mono.just(
|
||||
response.getChoices().get(0).getMessage().getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ChatCompletionsOptions createChatCompletionsOptions(
|
||||
ChatRequestSettings requestSettings, ChatHistory chat) {
|
||||
List<ChatMessage> messages =
|
||||
chat.getMessages().stream()
|
||||
.map(
|
||||
it ->
|
||||
new ChatMessage(
|
||||
toChatRole(it.getAuthorRoles()), it.getContent()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ChatCompletionsOptions options = new ChatCompletionsOptions(messages);
|
||||
|
||||
options.setMaxTokens(requestSettings.getMaxTokens());
|
||||
options.setTemperature(requestSettings.getTemperature());
|
||||
options.setTopP(requestSettings.getTopP());
|
||||
options.setFrequencyPenalty(requestSettings.getFrequencyPenalty());
|
||||
options.setPresencePenalty(requestSettings.getPresencePenalty());
|
||||
options.setLogitBias(new HashMap<>());
|
||||
options.setN(requestSettings.getBestOf());
|
||||
options.setUser(requestSettings.getUser());
|
||||
options.setStop(requestSettings.getStopSequences());
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static ChatRole toChatRole(ChatHistory.AuthorRoles authorRoles) {
|
||||
switch (authorRoles) {
|
||||
case System:
|
||||
return ChatRole.SYSTEM;
|
||||
case User:
|
||||
return ChatRole.USER;
|
||||
case Assistant:
|
||||
return ChatRole.ASSISTANT;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid chat message author: " + authorRoles.name());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAIChatHistory createNewChat(@Nullable String instructions) {
|
||||
return internalCreateNewChat(instructions, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> generateMessageStream(
|
||||
OpenAIChatHistory chat, @Nullable ChatRequestSettings requestSettings) {
|
||||
return this.getStreamingChatCompletionsAsync(chat, requestSettings)
|
||||
.groupBy(ChatCompletions::getId)
|
||||
.concatMap(
|
||||
chatCompletionResult -> {
|
||||
return chatCompletionResult
|
||||
.concatMap(
|
||||
chatCompletions ->
|
||||
Flux.fromIterable(chatCompletions.getChoices()))
|
||||
.reduce("", accumulateString());
|
||||
});
|
||||
}
|
||||
|
||||
private static BiFunction<String, ChatChoice, String> accumulateString() {
|
||||
return (newString, chatChoice) -> {
|
||||
ChatMessage message = chatChoice.getDelta();
|
||||
if (message != null && message.getContent() != null) {
|
||||
return newString + message.getContent();
|
||||
}
|
||||
return newString;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ChatCompletions> getStreamingChatCompletionsAsync(
|
||||
OpenAIChatHistory chat, ChatRequestSettings requestSettings) {
|
||||
return internalGetChatStreamingResultsAsync(chat, requestSettings);
|
||||
}
|
||||
|
||||
private Flux<ChatCompletions> internalGetChatStreamingResultsAsync(
|
||||
ChatHistory chat, @Nullable ChatRequestSettings requestSettings) {
|
||||
Verify.notNull(chat);
|
||||
if (requestSettings == null) {
|
||||
requestSettings = new ChatRequestSettings();
|
||||
}
|
||||
|
||||
ClientBase.validateMaxTokens(requestSettings.getMaxTokens());
|
||||
|
||||
ChatCompletionsOptions options = createChatCompletionsOptions(requestSettings, chat);
|
||||
options = options.setStream(true);
|
||||
|
||||
return getClient().getChatCompletionsStream(getModelId(), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new empty chat instance
|
||||
*
|
||||
* @param instructions Optional chat instructions for the AI service
|
||||
* @param userMessage Optional user message to start the chat
|
||||
* @return Chat object
|
||||
*/
|
||||
private static OpenAIChatHistory internalCreateNewChat(
|
||||
@Nullable String instructions, @Nullable String userMessage) {
|
||||
if (instructions == null) {
|
||||
instructions = "Assistant is a large language model.";
|
||||
}
|
||||
OpenAIChatHistory history = new OpenAIChatHistory(instructions);
|
||||
if (userMessage != null) {
|
||||
history.addUserMessage(userMessage);
|
||||
}
|
||||
return history;
|
||||
}
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.chatcompletion;
|
||||
|
||||
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** OpenAI Chat content */
|
||||
public class OpenAIChatHistory extends ChatHistory {
|
||||
|
||||
/**
|
||||
* Create a new and empty chat history
|
||||
*
|
||||
* @param assistantInstructions Optional instructions for the assistant
|
||||
*/
|
||||
public OpenAIChatHistory(@Nullable String assistantInstructions) {
|
||||
super();
|
||||
|
||||
if (assistantInstructions != null && !assistantInstructions.isEmpty()) {
|
||||
this.addSystemMessage(assistantInstructions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a system message to the chat history
|
||||
*
|
||||
* @param content Message content
|
||||
*/
|
||||
public void addSystemMessage(String content) {
|
||||
this.addMessage(AuthorRoles.System, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an assistant message to the chat history
|
||||
*
|
||||
* @param content Message content
|
||||
*/
|
||||
public void addAssistantMessage(String content) {
|
||||
this.addMessage(AuthorRoles.Assistant, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user message to the chat history
|
||||
*
|
||||
* @param content Message content
|
||||
*/
|
||||
public void addUserMessage(String content) {
|
||||
this.addMessage(AuthorRoles.User, content);
|
||||
}
|
||||
}
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.textcompletion;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.models.Choice;
|
||||
import com.azure.ai.openai.models.Completions;
|
||||
import com.azure.ai.openai.models.CompletionsOptions;
|
||||
import com.microsoft.semantickernel.Verify;
|
||||
import com.microsoft.semantickernel.ai.AIException;
|
||||
import com.microsoft.semantickernel.chatcompletion.ChatRequestSettings;
|
||||
import com.microsoft.semantickernel.connectors.ai.openai.azuresdk.ClientBase;
|
||||
import com.microsoft.semantickernel.exceptions.NotSupportedException;
|
||||
import com.microsoft.semantickernel.exceptions.NotSupportedException.ErrorCodes;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionRequestSettings;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionType;
|
||||
import com.microsoft.semantickernel.textcompletion.TextCompletion;
|
||||
import jakarta.inject.Inject;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI text completion service.
|
||||
/// TODO: forward ETW logging to ILogger, see
|
||||
// https://learn.microsoft.com/en-us/dotnet/azure/sdk/logging
|
||||
/// </summary>
|
||||
public class OpenAITextCompletion extends ClientBase implements TextCompletion {
|
||||
|
||||
private final CompletionType defaultCompletionType;
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of the OpenAI text completion connector
|
||||
/// </summary>
|
||||
/// <param name="modelId">Model name</param>
|
||||
/// <param name="apiKey">OpenAI API Key</param>
|
||||
/// <param name="organization">OpenAI Organization Id (usually optional)</param>
|
||||
/// <param name="handlerFactory">Retry handler factory for HTTP
|
||||
/// requests.</param>
|
||||
/// <param name="log">Application logger</param>
|
||||
@Inject
|
||||
public OpenAITextCompletion(OpenAIAsyncClient client, String modelId) {
|
||||
super(client, modelId);
|
||||
defaultCompletionType = CompletionType.STREAMING;
|
||||
}
|
||||
|
||||
public OpenAITextCompletion(
|
||||
OpenAIAsyncClient client, String modelId, CompletionType defaultCompletionType) {
|
||||
super(client, modelId);
|
||||
|
||||
this.defaultCompletionType = defaultCompletionType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<String>> completeAsync(
|
||||
@Nonnull String text, @Nonnull CompletionRequestSettings requestSettings) {
|
||||
return this.internalCompleteTextAsync(text, requestSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> completeStreamAsync(
|
||||
@Nonnull String text, @Nonnull CompletionRequestSettings requestSettings) {
|
||||
CompletionsOptions completionsOptions = getCompletionsOptions(text, requestSettings);
|
||||
|
||||
return generateMessageStream(completionsOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionType defaultCompletionType() {
|
||||
return defaultCompletionType;
|
||||
}
|
||||
|
||||
private Flux<String> generateMessageStream(CompletionsOptions completionsOptions) {
|
||||
return getClient()
|
||||
.getCompletionsStream(getModelId(completionsOptions), completionsOptions)
|
||||
.groupBy(Completions::getId)
|
||||
.concatMap(
|
||||
completionResult -> {
|
||||
return completionResult
|
||||
.concatMap(
|
||||
completion ->
|
||||
Flux.fromIterable(completion.getChoices()))
|
||||
.reduce("", accumulateString());
|
||||
});
|
||||
}
|
||||
|
||||
private static BiFunction<String, Choice, String> accumulateString() {
|
||||
return (newText, choice) -> {
|
||||
String message = choice.getText();
|
||||
if (!Verify.isNullOrEmpty(message)) {
|
||||
return newText + message;
|
||||
}
|
||||
return newText;
|
||||
};
|
||||
}
|
||||
|
||||
protected Mono<List<String>> internalCompleteTextAsync(
|
||||
String text, CompletionRequestSettings requestSettings) {
|
||||
CompletionsOptions completionsOptions = getCompletionsOptions(text, requestSettings);
|
||||
|
||||
return getClient()
|
||||
.getCompletions(getModelId(requestSettings), completionsOptions)
|
||||
.flatMapIterable(Completions::getChoices)
|
||||
.mapNotNull(Choice::getText)
|
||||
.collectList();
|
||||
}
|
||||
|
||||
private String getModelId(CompletionRequestSettings requestSettings) {
|
||||
String model = requestSettings.getModelId();
|
||||
if (model == null) {
|
||||
model = getModelId();
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private String getModelId(CompletionsOptions completionsOptions) {
|
||||
String model = completionsOptions.getModel();
|
||||
if (model == null) {
|
||||
model = getModelId();
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private CompletionsOptions getCompletionsOptions(
|
||||
String text, CompletionRequestSettings requestSettings) {
|
||||
if (requestSettings.getMaxTokens() < 1) {
|
||||
throw new AIException(AIException.ErrorCodes.INVALID_REQUEST, "Max tokens must be >0");
|
||||
}
|
||||
|
||||
CompletionsOptions options =
|
||||
new CompletionsOptions(Collections.singletonList(text))
|
||||
.setMaxTokens(requestSettings.getMaxTokens())
|
||||
.setTemperature(requestSettings.getTemperature())
|
||||
.setTopP(requestSettings.getTopP())
|
||||
.setFrequencyPenalty(requestSettings.getFrequencyPenalty())
|
||||
.setPresencePenalty(requestSettings.getPresencePenalty())
|
||||
.setModel(getModelId(requestSettings))
|
||||
.setUser(requestSettings.getUser())
|
||||
.setBestOf(requestSettings.getBestOf())
|
||||
.setLogitBias(new HashMap<>());
|
||||
|
||||
if (requestSettings instanceof ChatRequestSettings) {
|
||||
options = options.setStop(requestSettings.getStopSequences());
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
public static final class Builder implements TextCompletion.Builder {
|
||||
|
||||
@Nullable private OpenAIAsyncClient client;
|
||||
@Nullable private String modelId;
|
||||
private CompletionType defaultCompletionType = CompletionType.STREAMING;
|
||||
|
||||
public Builder withOpenAIClient(OpenAIAsyncClient client) {
|
||||
this.client = client;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withModelId(String modelId) {
|
||||
this.modelId = modelId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder withDefaultCompletionType(CompletionType completionType) {
|
||||
this.defaultCompletionType = completionType;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextCompletion build() {
|
||||
if (client == null) {
|
||||
throw new NotSupportedException(ErrorCodes.NOT_SUPPORTED, "OpenAI client not set");
|
||||
}
|
||||
if (modelId == null) {
|
||||
throw new NotSupportedException(ErrorCodes.NOT_SUPPORTED, "Model ID not set");
|
||||
}
|
||||
return new OpenAITextCompletion(client, modelId, defaultCompletionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.textembeddings;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.models.EmbeddingItem;
|
||||
import com.azure.ai.openai.models.Embeddings;
|
||||
import com.azure.ai.openai.models.EmbeddingsOptions;
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.ai.embeddings.TextEmbeddingGeneration;
|
||||
import com.microsoft.semantickernel.connectors.ai.openai.azuresdk.ClientBase;
|
||||
import com.microsoft.semantickernel.exceptions.NotSupportedException;
|
||||
import com.microsoft.semantickernel.exceptions.NotSupportedException.ErrorCodes;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class OpenAITextEmbeddingGeneration extends ClientBase implements TextEmbeddingGeneration {
|
||||
|
||||
public OpenAITextEmbeddingGeneration(OpenAIAsyncClient client, String modelId) {
|
||||
super(client, modelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<Embedding>> generateEmbeddingsAsync(List<String> data) {
|
||||
return this.internalGenerateTextEmbeddingsAsync(data);
|
||||
}
|
||||
|
||||
protected Mono<List<Embedding>> internalGenerateTextEmbeddingsAsync(List<String> data) {
|
||||
EmbeddingsOptions options =
|
||||
new EmbeddingsOptions(data).setModel(getModelId()).setUser("default");
|
||||
|
||||
return getClient()
|
||||
.getEmbeddings(getModelId(), options)
|
||||
.flatMapIterable(Embeddings::getData)
|
||||
.mapNotNull(EmbeddingItem::getEmbedding)
|
||||
.map(
|
||||
embedding ->
|
||||
embedding.stream()
|
||||
.map(Double::floatValue)
|
||||
.collect(Collectors.toList()))
|
||||
.mapNotNull(Embedding::new)
|
||||
.collectList();
|
||||
}
|
||||
|
||||
public static class Builder implements TextEmbeddingGeneration.Builder {
|
||||
private OpenAIAsyncClient client;
|
||||
private String modelId;
|
||||
|
||||
public Builder withOpenAIClient(OpenAIAsyncClient client) {
|
||||
this.client = client;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withModelId(String modelId) {
|
||||
this.modelId = modelId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAITextEmbeddingGeneration build() {
|
||||
if (client == null) {
|
||||
throw new NotSupportedException(ErrorCodes.NOT_SUPPORTED, "OpenAI client not set");
|
||||
}
|
||||
if (modelId == null) {
|
||||
throw new NotSupportedException(ErrorCodes.NOT_SUPPORTED, "Model ID not set");
|
||||
}
|
||||
return new OpenAITextEmbeddingGeneration(client, modelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
com.microsoft.semantickernel.connectors.ai.openai.textembeddings.OpenAITextEmbeddingGeneration$Builder
|
||||
-1
@@ -1 +0,0 @@
|
||||
com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatCompletion$Builder
|
||||
-1
@@ -1 +0,0 @@
|
||||
com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion$Builder
|
||||
@@ -1,78 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-connectors-memory-azurecognitivesearch</artifactId>
|
||||
|
||||
<name>Semantic Kernel Azure Cognitive Search Memory Connector</name>
|
||||
<description>Defines connector for Azure Cognitive Search to be used with the Semantic Kernel</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.azure</groupId>
|
||||
<artifactId>azure-ai-openai</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.azure</groupId>
|
||||
<artifactId>azure-search-documents</artifactId>
|
||||
<exclusions>
|
||||
<!--
|
||||
TODO: Remove pending a fix for:
|
||||
https://github.com/Azure/azure-sdk-for-java/issues/36054
|
||||
-->
|
||||
<exclusion>
|
||||
<groupId>com.azure</groupId>
|
||||
<artifactId>azure-core-serializer-json-jackson</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j2-impl</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-settings-loader</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-ai-openai</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.azurecognitivesearch;
|
||||
|
||||
import com.microsoft.semantickernel.SKException;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class AzureCognitiveSearchMemoryException extends SKException {
|
||||
private final ErrorCodes errorCode;
|
||||
|
||||
/**
|
||||
* Create an exception with a message
|
||||
*
|
||||
* @param errorCode The error code
|
||||
*/
|
||||
public AzureCognitiveSearchMemoryException(@Nonnull ErrorCodes errorCode) {
|
||||
this(errorCode, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an exception with a message
|
||||
*
|
||||
* @param message a description of the cause of the exception
|
||||
*/
|
||||
public AzureCognitiveSearchMemoryException(
|
||||
@Nonnull ErrorCodes errorCode, @Nullable String message) {
|
||||
this(errorCode, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an exception with a message and a cause
|
||||
*
|
||||
* @param errorCode the error code
|
||||
* @param message a description of the cause of the exception
|
||||
* @param cause the cause of the exception
|
||||
*/
|
||||
public AzureCognitiveSearchMemoryException(
|
||||
@Nonnull ErrorCodes errorCode, @Nullable String message, @Nullable Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ErrorCodes getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public enum ErrorCodes {
|
||||
UNKNOWN_ERROR("Unknown error"),
|
||||
INVALID_EMBEDDING_SIZE("Invalid embedding size"),
|
||||
INVALID_INDEX_NAME("Invalid index name"),
|
||||
READ_FAILURE("Read failure"),
|
||||
WRITE_FAILURE("Write failure");
|
||||
|
||||
final String message;
|
||||
|
||||
ErrorCodes(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.azurecognitivesearch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecord;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecordMetadata;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Azure Cognitive Search record and index definition. Note: once defined, index cannot be modified.
|
||||
*/
|
||||
public final class AzureCognitiveSearchMemoryRecord {
|
||||
|
||||
static final String ID = "Id";
|
||||
static final String TEXT = "Text";
|
||||
static final String DESCRIPTION = "Description";
|
||||
static final String ADDITIONAL_METADATA = "AdditionalMetadata";
|
||||
static final String EMBEDDING = "Embedding";
|
||||
static final String EXTERNAL_SOURCE_NAME = "ExternalSourceName";
|
||||
static final String IS_REFERENCE = "Reference";
|
||||
|
||||
private final String id;
|
||||
|
||||
private final String text;
|
||||
|
||||
private final String description;
|
||||
|
||||
private final String additionalMetadata;
|
||||
|
||||
private final List<Float> embedding;
|
||||
|
||||
private final String externalSourceName;
|
||||
|
||||
private final boolean isReference;
|
||||
|
||||
@JsonCreator
|
||||
public AzureCognitiveSearchMemoryRecord(
|
||||
@JsonProperty(ID) String id,
|
||||
@JsonProperty(TEXT) String text,
|
||||
@JsonProperty(DESCRIPTION) String description,
|
||||
@JsonProperty(ADDITIONAL_METADATA) String additionalMetadata,
|
||||
@JsonProperty(EMBEDDING) List<Float> embedding,
|
||||
@JsonProperty(EXTERNAL_SOURCE_NAME) String externalSourceName,
|
||||
@JsonProperty(IS_REFERENCE) boolean isReference) {
|
||||
this.id = id;
|
||||
this.text = text;
|
||||
this.description = description;
|
||||
this.additionalMetadata = additionalMetadata;
|
||||
this.embedding = embedding != null ? embedding : Collections.emptyList();
|
||||
this.externalSourceName = externalSourceName;
|
||||
this.isReference = isReference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record ID. The record is not filterable to save quota, also SK uses only semantic search.
|
||||
*
|
||||
* @return Record ID.
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content is stored here.
|
||||
*
|
||||
* @return Content is stored here, {@code null} if not set.
|
||||
*/
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional description of the content, e.g. a title. This can be useful when indexing external
|
||||
* data without pulling in the entire content.
|
||||
*
|
||||
* @return Optional description of the content, {@code null} if not set.
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional metadata. Currently, this is a String where you could store serialized data as
|
||||
* JSON. In future the design might change to allow storing named values and leverage filters.
|
||||
*
|
||||
* @return Additional metadata, {@code null} if not set.
|
||||
*/
|
||||
public String getAdditionalMetadata() {
|
||||
return additionalMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedding vector.
|
||||
*
|
||||
* @return Embedding vector.
|
||||
*/
|
||||
public List<Float> getEmbedding() {
|
||||
return embedding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the external source, in cases where the content and the Id are referenced to external
|
||||
* information.
|
||||
*
|
||||
* @return Name of the external source, in cases where the content and the Id are referenced to
|
||||
* external information, {@code null} if not set.
|
||||
*/
|
||||
public String getExternalSourceName() {
|
||||
return externalSourceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the record references external information.
|
||||
*
|
||||
* @return {@code true} if the record references external information, {@code false} otherwise.
|
||||
*/
|
||||
public boolean isReference() {
|
||||
return isReference;
|
||||
}
|
||||
|
||||
public MemoryRecordMetadata toMemoryRecordMetadata() {
|
||||
return new MemoryRecordMetadata(
|
||||
this.isReference,
|
||||
decodeId(this.id),
|
||||
this.text,
|
||||
this.description,
|
||||
this.externalSourceName,
|
||||
this.additionalMetadata);
|
||||
}
|
||||
|
||||
public static AzureCognitiveSearchMemoryRecord fromMemoryRecord(MemoryRecord record) {
|
||||
return new AzureCognitiveSearchMemoryRecord(
|
||||
record.getMetadata().getId(),
|
||||
record.getMetadata().getText(),
|
||||
record.getMetadata().getDescription(),
|
||||
record.getMetadata().getAdditionalMetadata(),
|
||||
record.getEmbedding().getVector(),
|
||||
record.getMetadata().getExternalSourceName(),
|
||||
record.getMetadata().isReference());
|
||||
}
|
||||
|
||||
public MemoryRecord toMemoryRecord() {
|
||||
return toMemoryRecord(true);
|
||||
}
|
||||
|
||||
public MemoryRecord toMemoryRecord(boolean withEmbeddings) {
|
||||
return new MemoryRecord(
|
||||
this.toMemoryRecordMetadata(),
|
||||
new Embedding(withEmbeddings ? this.embedding : Collections.emptyList()),
|
||||
this.id,
|
||||
null);
|
||||
}
|
||||
|
||||
// ACS keys can contain only letters, digits, underscore, dash, equal sign, recommending
|
||||
// to encode values with a URL-safe algorithm.
|
||||
// <param name="realId">Original Id</param>
|
||||
// <returns>Encoded id</returns>
|
||||
static String encodeId(@Nullable String realId) {
|
||||
if (realId == null) {
|
||||
return "";
|
||||
}
|
||||
byte[] bytes = Base64.getUrlEncoder().encode(realId.getBytes(StandardCharsets.UTF_8));
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static String decodeId(@Nullable String encodedId) {
|
||||
if (encodedId == null) {
|
||||
return "";
|
||||
}
|
||||
byte[] bytes = Base64.getUrlDecoder().decode(encodedId.getBytes(StandardCharsets.UTF_8));
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
-510
@@ -1,510 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.azurecognitivesearch;
|
||||
|
||||
import com.azure.core.credential.AzureKeyCredential;
|
||||
import com.azure.core.credential.TokenCredential;
|
||||
import com.azure.core.http.rest.Response;
|
||||
import com.azure.core.util.ClientOptions;
|
||||
import com.azure.core.util.MetricsOptions;
|
||||
import com.azure.core.util.TracingOptions;
|
||||
import com.azure.search.documents.SearchAsyncClient;
|
||||
import com.azure.search.documents.SearchDocument;
|
||||
import com.azure.search.documents.indexes.SearchIndexAsyncClient;
|
||||
import com.azure.search.documents.indexes.SearchIndexClientBuilder;
|
||||
import com.azure.search.documents.indexes.models.HnswParameters;
|
||||
import com.azure.search.documents.indexes.models.HnswVectorSearchAlgorithmConfiguration;
|
||||
import com.azure.search.documents.indexes.models.LexicalAnalyzerName;
|
||||
import com.azure.search.documents.indexes.models.SearchField;
|
||||
import com.azure.search.documents.indexes.models.SearchFieldDataType;
|
||||
import com.azure.search.documents.indexes.models.SearchIndex;
|
||||
import com.azure.search.documents.indexes.models.VectorSearch;
|
||||
import com.azure.search.documents.indexes.models.VectorSearchAlgorithmMetric;
|
||||
import com.azure.search.documents.models.IndexDocumentsOptions;
|
||||
import com.azure.search.documents.models.IndexingResult;
|
||||
import com.azure.search.documents.models.SearchOptions;
|
||||
import com.azure.search.documents.models.SearchQueryVector;
|
||||
import com.microsoft.semantickernel.SemanticKernelHttpSettings;
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.connectors.memory.azurecognitivesearch.AzureCognitiveSearchMemoryException.ErrorCodes;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecord;
|
||||
import com.microsoft.semantickernel.memory.MemoryStore;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
/**
|
||||
* Semantic Memory implementation using Azure Cognitive Search. For more information about Azure
|
||||
* Cognitive Search {@see https://learn.microsoft.com/azure/search/search-what-is-azure-search}
|
||||
*/
|
||||
public class AzureCognitiveSearchMemoryStore implements MemoryStore {
|
||||
private static final String SEARCH_CONFIG_NAME = "searchConfig";
|
||||
|
||||
private final SearchIndexAsyncClient _adminClient;
|
||||
|
||||
private final Map<String, SearchAsyncClient> _clientsByIndex = new ConcurrentHashMap<>();
|
||||
private final Function<SearchDocument, MemoryRecord> memoryRecordMapper;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of memory storage using Azure Cognitive Search.
|
||||
/// </summary>
|
||||
/// <param name="endpoint">Azure Cognitive Search URI, e.g.
|
||||
// "https://contoso.search.windows.net"</param>
|
||||
/// <param name="apiKey">API Key</param>
|
||||
|
||||
/**
|
||||
* Create a new instance of memory storage using Azure Cognitive Search.
|
||||
*
|
||||
* @param endpoint Azure Cognitive Search URI, e.g. "https://contoso.search.windows.net"
|
||||
* @param apiKey Azure API Key
|
||||
*/
|
||||
public AzureCognitiveSearchMemoryStore(@Nonnull String endpoint, @Nonnull String apiKey) {
|
||||
this(
|
||||
new SearchIndexClientBuilder()
|
||||
.endpoint(endpoint)
|
||||
.credential(new AzureKeyCredential(apiKey))
|
||||
.clientOptions(clientOptions())
|
||||
.buildAsyncClient(),
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of memory storage using Azure Cognitive Search.
|
||||
*
|
||||
* @param endpoint Azure Cognitive Search URI, e.g. "https://contoso.search.windows.net"
|
||||
* @param credentials Azure service credentials
|
||||
*/
|
||||
public AzureCognitiveSearchMemoryStore(
|
||||
@Nonnull String endpoint, @Nonnull TokenCredential credentials) {
|
||||
this(
|
||||
new SearchIndexClientBuilder()
|
||||
.endpoint(endpoint)
|
||||
.credential(credentials)
|
||||
.clientOptions(clientOptions())
|
||||
.buildAsyncClient(),
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of memory storage using Azure Cognitive Search.
|
||||
*
|
||||
* @param searchIndexAsyncClient the Azure search documents index client to use
|
||||
*/
|
||||
public AzureCognitiveSearchMemoryStore(
|
||||
@Nonnull SearchIndexAsyncClient searchIndexAsyncClient,
|
||||
@Nullable Function<SearchDocument, MemoryRecord> memoryRecordMapper) {
|
||||
this._adminClient = searchIndexAsyncClient;
|
||||
this.memoryRecordMapper = memoryRecordMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> createCollectionAsync(@Nonnull String collectionName) {
|
||||
// Indexes are created when sending a record. The creation requires the size of the
|
||||
// embeddings.
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<String>> getCollectionsAsync() {
|
||||
return this.getIndexesAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> doesCollectionExistAsync(@Nonnull String collectionName) {
|
||||
String normalizedIndexName = normalizeIndexName(collectionName);
|
||||
return getIndexesAsync()
|
||||
.map(
|
||||
list ->
|
||||
list.stream()
|
||||
.anyMatch(
|
||||
name ->
|
||||
name.equalsIgnoreCase(collectionName)
|
||||
|| name.equalsIgnoreCase(
|
||||
normalizedIndexName)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteCollectionAsync(@Nonnull String collectionName) {
|
||||
collectionName = normalizeIndexName(collectionName);
|
||||
return _adminClient.deleteIndex(collectionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> upsertAsync(@Nonnull String collectionName, @Nonnull MemoryRecord record) {
|
||||
collectionName = normalizeIndexName(collectionName);
|
||||
return this.upsertRecordAsync(
|
||||
collectionName, AzureCognitiveSearchMemoryRecord.fromMemoryRecord(record));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<String>> upsertBatchAsync(
|
||||
@Nonnull String collectionName, Collection<MemoryRecord> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return Mono.just(Collections.emptyList());
|
||||
}
|
||||
|
||||
collectionName = normalizeIndexName(collectionName);
|
||||
List<AzureCognitiveSearchMemoryRecord> searchRecords =
|
||||
records.stream()
|
||||
.map(AzureCognitiveSearchMemoryRecord::fromMemoryRecord)
|
||||
.collect(Collectors.toList());
|
||||
return upsertBatchAsync(collectionName, searchRecords);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MemoryRecord> getAsync(
|
||||
@Nonnull String collectionName, @Nonnull String key, boolean withEmbedding) {
|
||||
collectionName = normalizeIndexName(collectionName);
|
||||
key = AzureCognitiveSearchMemoryRecord.encodeId(key);
|
||||
SearchAsyncClient client = this.getSearchClient(collectionName);
|
||||
|
||||
return client.getDocumentWithResponse(key, AzureCognitiveSearchMemoryRecord.class, null)
|
||||
.map(
|
||||
response -> {
|
||||
if (response.getStatusCode() == 404) {
|
||||
throw new AzureCognitiveSearchMemoryException(
|
||||
ErrorCodes.READ_FAILURE, "Index not found");
|
||||
}
|
||||
return response.getValue().toMemoryRecord(withEmbedding);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<MemoryRecord>> getBatchAsync(
|
||||
@Nonnull String collectionName,
|
||||
@Nonnull Collection<String> keys,
|
||||
boolean withEmbedding) {
|
||||
// TODO: there _must_ be a better way than one request per key!
|
||||
return Flux.fromIterable(keys)
|
||||
.flatMap(key -> getAsync(collectionName, key, withEmbedding))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Tuple2<MemoryRecord, Float>> getNearestMatchAsync(
|
||||
@Nonnull String collectionName,
|
||||
@Nonnull Embedding embedding,
|
||||
float minRelevanceScore,
|
||||
boolean withEmbedding) {
|
||||
return getNearestMatchesAsync(
|
||||
collectionName, embedding, 1, minRelevanceScore, withEmbedding)
|
||||
.map(matches -> matches.iterator().next());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<Tuple2<MemoryRecord, Float>>> getNearestMatchesAsync(
|
||||
@Nonnull String collectionName,
|
||||
@Nonnull Embedding embedding,
|
||||
int limit,
|
||||
float minRelevanceScore,
|
||||
boolean withEmbedding) {
|
||||
return getNearestMatchesAsync(
|
||||
collectionName,
|
||||
embedding,
|
||||
limit,
|
||||
minRelevanceScore,
|
||||
withEmbedding,
|
||||
memoryRecordMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the nearest matches to the {@link Embedding} of type {@code Float}. Does not guarantee
|
||||
* that the collection exists.
|
||||
*
|
||||
* <p>If a memoryRecordMapper is provided this overrides any mapper provided when constructing
|
||||
* this memory store.
|
||||
*
|
||||
* @param collectionName The name associated with a collection of embeddings.
|
||||
* @param embedding The {@link Embedding} to compare the collection's embeddings with.
|
||||
* @param limit The maximum number of similarity results to return.
|
||||
* @param minRelevanceScore The minimum relevance threshold for returned results.
|
||||
* @param withEmbedding If true, the embeddings will be returned in the memory records.
|
||||
* @param memoryRecordMapper a mapper that controls how to map the search document to a memory
|
||||
* record
|
||||
* @return A collection of tuples where item1 is a {@link MemoryRecord} and item2 is its
|
||||
* similarity score as a {@code Float}.
|
||||
*/
|
||||
public Mono<Collection<Tuple2<MemoryRecord, Float>>> getNearestMatchesAsync(
|
||||
@Nonnull String collectionName,
|
||||
@Nonnull Embedding embedding,
|
||||
int limit,
|
||||
float minRelevanceScore,
|
||||
boolean withEmbedding,
|
||||
@Nullable Function<SearchDocument, MemoryRecord> memoryRecordMapper) {
|
||||
collectionName = normalizeIndexName(collectionName);
|
||||
SearchAsyncClient client = getSearchClient(collectionName);
|
||||
|
||||
SearchQueryVector searchVector =
|
||||
new SearchQueryVector()
|
||||
.setKNearestNeighborsCount(limit)
|
||||
.setFields(AzureCognitiveSearchMemoryRecord.EMBEDDING)
|
||||
.setValue(embedding.getVector());
|
||||
|
||||
SearchOptions searchOptions = new SearchOptions().setVectors(searchVector);
|
||||
|
||||
return client.search(null, searchOptions)
|
||||
.filter(result -> (double) minRelevanceScore <= result.getScore())
|
||||
.map(
|
||||
result -> {
|
||||
MemoryRecord memoryRecord;
|
||||
if (memoryRecordMapper != null) {
|
||||
SearchDocument doc = result.getDocument(SearchDocument.class);
|
||||
memoryRecord = memoryRecordMapper.apply(doc);
|
||||
} else {
|
||||
AzureCognitiveSearchMemoryRecord doc =
|
||||
result.getDocument(AzureCognitiveSearchMemoryRecord.class);
|
||||
memoryRecord = doc.toMemoryRecord(withEmbedding);
|
||||
}
|
||||
float score = (float) result.getScore();
|
||||
return Tuples.of(memoryRecord, score);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeAsync(@Nonnull String collectionName, @Nonnull String key) {
|
||||
return removeBatchAsync(collectionName, Collections.singleton(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeBatchAsync(
|
||||
@Nonnull String collectionName, @Nonnull Collection<String> keys) {
|
||||
collectionName = normalizeIndexName(collectionName);
|
||||
SearchAsyncClient client = this.getSearchClient(collectionName);
|
||||
|
||||
List<SearchDocument> documents =
|
||||
keys.stream()
|
||||
.map(AzureCognitiveSearchMemoryRecord::encodeId)
|
||||
.map(
|
||||
key -> {
|
||||
SearchDocument searchDocument = new SearchDocument();
|
||||
searchDocument.put("Id", key); // TODO: Id should be a constant
|
||||
return searchDocument;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return client.deleteDocuments(documents).then();
|
||||
}
|
||||
|
||||
static List<SearchField> searchFields(String configName, int embeddingSize) {
|
||||
return Arrays.asList(
|
||||
new SearchField(AzureCognitiveSearchMemoryRecord.ID, SearchFieldDataType.STRING)
|
||||
.setKey(true)
|
||||
.setFilterable(false),
|
||||
new SearchField(
|
||||
AzureCognitiveSearchMemoryRecord.EMBEDDING,
|
||||
SearchFieldDataType.collection(SearchFieldDataType.SINGLE))
|
||||
.setSearchable(true)
|
||||
.setVectorSearchDimensions(embeddingSize)
|
||||
.setVectorSearchConfiguration(configName),
|
||||
new SearchField(AzureCognitiveSearchMemoryRecord.TEXT, SearchFieldDataType.STRING)
|
||||
.setSearchable(true)
|
||||
.setFacetable(true)
|
||||
.setAnalyzerName(LexicalAnalyzerName.EN_LUCENE),
|
||||
new SearchField(
|
||||
AzureCognitiveSearchMemoryRecord.DESCRIPTION,
|
||||
SearchFieldDataType.STRING)
|
||||
.setSearchable(true)
|
||||
.setFacetable(true)
|
||||
.setAnalyzerName(LexicalAnalyzerName.EN_LUCENE),
|
||||
new SearchField(
|
||||
AzureCognitiveSearchMemoryRecord.ADDITIONAL_METADATA,
|
||||
SearchFieldDataType.STRING)
|
||||
.setSearchable(true)
|
||||
.setFacetable(true)
|
||||
.setAnalyzerName(LexicalAnalyzerName.EN_LUCENE),
|
||||
new SearchField(
|
||||
AzureCognitiveSearchMemoryRecord.EXTERNAL_SOURCE_NAME,
|
||||
SearchFieldDataType.STRING)
|
||||
.setSearchable(true)
|
||||
.setFacetable(true)
|
||||
.setAnalyzerName(LexicalAnalyzerName.EN_LUCENE),
|
||||
new SearchField(
|
||||
AzureCognitiveSearchMemoryRecord.IS_REFERENCE,
|
||||
SearchFieldDataType.BOOLEAN)
|
||||
.setFilterable(true)
|
||||
.setFacetable(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new search index.
|
||||
*
|
||||
* @param indexName Index name
|
||||
* @param embeddingSize Size of the embedding vector
|
||||
* @return A Mono that completes when the index is created
|
||||
*/
|
||||
private Mono<Response<SearchIndex>> createIndexAsync(
|
||||
@Nonnull String indexName, int embeddingSize) {
|
||||
if (embeddingSize < 1) {
|
||||
throw new AzureCognitiveSearchMemoryException(
|
||||
ErrorCodes.INVALID_EMBEDDING_SIZE, "the value must be greater than zero");
|
||||
}
|
||||
|
||||
String normalizedIndexName = normalizeIndexName(indexName);
|
||||
|
||||
VectorSearch vectorSearch =
|
||||
new VectorSearch()
|
||||
.setAlgorithmConfigurations(
|
||||
Collections.singletonList(
|
||||
new HnswVectorSearchAlgorithmConfiguration(
|
||||
SEARCH_CONFIG_NAME)
|
||||
.setParameters(
|
||||
new HnswParameters()
|
||||
.setMetric(
|
||||
VectorSearchAlgorithmMetric
|
||||
.COSINE))));
|
||||
|
||||
SearchIndex newIndex =
|
||||
new SearchIndex(normalizedIndexName)
|
||||
.setFields(searchFields(SEARCH_CONFIG_NAME, embeddingSize))
|
||||
.setVectorSearch(vectorSearch);
|
||||
|
||||
return _adminClient.createIndexWithResponse(newIndex);
|
||||
}
|
||||
|
||||
private Mono<List<String>> getIndexesAsync() {
|
||||
return _adminClient.listIndexes().map(SearchIndex::getName).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Mono<String> upsertRecordAsync(
|
||||
@Nonnull String indexName, @Nonnull AzureCognitiveSearchMemoryRecord record) {
|
||||
return upsertBatchAsync(indexName, Collections.singletonList(record))
|
||||
.map(Collection::iterator)
|
||||
.map(Iterator::next);
|
||||
}
|
||||
|
||||
private Mono<Collection<String>> upsertBatchAsync(
|
||||
@Nonnull String indexName, @Nonnull List<AzureCognitiveSearchMemoryRecord> records) {
|
||||
|
||||
if (records.isEmpty()) {
|
||||
return Mono.just(Collections.emptyList());
|
||||
}
|
||||
|
||||
List<SearchDocument> documents =
|
||||
records.stream()
|
||||
.map(
|
||||
record -> {
|
||||
SearchDocument searchDocument = new SearchDocument();
|
||||
searchDocument.put(
|
||||
AzureCognitiveSearchMemoryRecord.ID,
|
||||
AzureCognitiveSearchMemoryRecord.encodeId(
|
||||
record.getId()));
|
||||
searchDocument.put(
|
||||
AzureCognitiveSearchMemoryRecord.TEXT,
|
||||
record.getText());
|
||||
searchDocument.put(
|
||||
AzureCognitiveSearchMemoryRecord.DESCRIPTION,
|
||||
record.getDescription());
|
||||
searchDocument.put(
|
||||
AzureCognitiveSearchMemoryRecord.EMBEDDING,
|
||||
record.getEmbedding());
|
||||
searchDocument.put(
|
||||
AzureCognitiveSearchMemoryRecord.ADDITIONAL_METADATA,
|
||||
record.getAdditionalMetadata());
|
||||
searchDocument.put(
|
||||
AzureCognitiveSearchMemoryRecord.EXTERNAL_SOURCE_NAME,
|
||||
record.getExternalSourceName());
|
||||
searchDocument.put(
|
||||
AzureCognitiveSearchMemoryRecord.IS_REFERENCE,
|
||||
record.isReference());
|
||||
return searchDocument;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
IndexDocumentsOptions options = new IndexDocumentsOptions().setThrowOnAnyError(true);
|
||||
|
||||
int embeddingSize =
|
||||
records.stream()
|
||||
.map(AzureCognitiveSearchMemoryRecord::getEmbedding)
|
||||
.map(List::size)
|
||||
.max(Integer::compareTo)
|
||||
.orElse(0);
|
||||
|
||||
String normalizedIndexName = normalizeIndexName(indexName);
|
||||
SearchAsyncClient client = this.getSearchClient(normalizedIndexName);
|
||||
|
||||
return client.uploadDocumentsWithResponse(documents, options)
|
||||
.map(
|
||||
response -> {
|
||||
if (response.getStatusCode() == 404) {
|
||||
throw new AzureCognitiveSearchMemoryException(
|
||||
ErrorCodes.INVALID_INDEX_NAME);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.onErrorResume(
|
||||
e ->
|
||||
createIndexAsync(normalizedIndexName, embeddingSize)
|
||||
.then(
|
||||
client.uploadDocumentsWithResponse(
|
||||
documents, options)))
|
||||
.map(
|
||||
response ->
|
||||
response.getValue().getResults().stream()
|
||||
.map(IndexingResult::getKey)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a search client for the index specified. Note: the index might not exist, but we avoid
|
||||
* checking everytime and the extra latency.
|
||||
*
|
||||
* @param indexName Index name
|
||||
* @return Search client ready to read/write
|
||||
*/
|
||||
protected SearchAsyncClient getSearchClient(@Nonnull String indexName) {
|
||||
String normalizedIndexName = normalizeIndexName(indexName);
|
||||
return _clientsByIndex.computeIfAbsent(
|
||||
normalizedIndexName, _adminClient::getSearchAsyncClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options used by the Azure Cognitive Search client, e.g. User Agent.
|
||||
/// See also
|
||||
// https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/src/DiagnosticsOptions.cs
|
||||
/// </summary>
|
||||
private static ClientOptions clientOptions() {
|
||||
return SemanticKernelHttpSettings.getUserAgent()
|
||||
.setTracingOptions(new TracingOptions())
|
||||
.setMetricsOptions(new MetricsOptions());
|
||||
}
|
||||
|
||||
// Index names cannot contain special chars. We use this rule to replace a few common ones
|
||||
// with an underscore and reduce the chance of errors. If other special chars are used, we leave
|
||||
// it
|
||||
// to the service to throw an error.
|
||||
// Note:
|
||||
// - replacing chars introduces a small chance of conflicts, e.g. "the-user" and "the_user".
|
||||
// - we should consider whether making this optional and leave it to the developer to handle.
|
||||
//
|
||||
// Java 8 does not support \s for whitespace, so we have to list them all.
|
||||
private static final String whitespace = " |\\t|\\n|\\x0B|\\f|\\r";
|
||||
private static final String s_replaceIndexNameSymbolsRegex = "[" + whitespace + "|\\|/|.|_|:]";
|
||||
|
||||
// Normalize index name to match ACS rules.
|
||||
// See https://learn.microsoft.com/en-us/rest/api/searchservice/Create-Index
|
||||
// The method doesn't handle all the error scenarios, leaving it to the service
|
||||
// to throw an error for edge cases not handled locally.
|
||||
// <param name="indexName">Value to normalize</param>
|
||||
// <returns>Normalized name</returns>
|
||||
private static String normalizeIndexName(String indexName) {
|
||||
if (indexName.length() > 128) {
|
||||
throw new IllegalArgumentException("The collection name cannot exceed 128 chars");
|
||||
}
|
||||
|
||||
indexName = indexName.toLowerCase(Locale.ROOT);
|
||||
return indexName.replaceAll(s_replaceIndexNameSymbolsRegex, "-");
|
||||
}
|
||||
}
|
||||
-434
@@ -1,434 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.azurecognitivesearch;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.azure.core.http.HttpHeaders;
|
||||
import com.azure.core.http.HttpPipeline;
|
||||
import com.azure.core.http.HttpRequest;
|
||||
import com.azure.core.http.HttpResponse;
|
||||
import com.azure.core.http.rest.Response;
|
||||
import com.azure.core.util.Context;
|
||||
import com.azure.core.util.tracing.Tracer;
|
||||
import com.azure.search.documents.SearchAsyncClient;
|
||||
import com.azure.search.documents.SearchDocument;
|
||||
import com.azure.search.documents.indexes.SearchIndexAsyncClient;
|
||||
import com.azure.search.documents.models.SearchResult;
|
||||
import com.azure.search.documents.util.SearchPagedFlux;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration;
|
||||
import com.microsoft.semantickernel.memory.MemoryQueryResult;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecord;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecordMetadata;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
class AzureCognitiveSearchMemoryTests {
|
||||
|
||||
// com.azure.core.implementation.http.rest.RestProxyBase.RestProxyBase
|
||||
// expects a non-null tracer. There is a TracerProvider in the Azure SDK
|
||||
// that can be used to create a tracer, but it has some ceremony around
|
||||
// it. We create a simple no-op tracer instead.
|
||||
private static class NoOpTracer implements Tracer {
|
||||
|
||||
@Override
|
||||
public Context start(String methodName, Context context) {
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end(String errorMessage, Throwable throwable, Context context) {}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String key, String value, Context context) {}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
// we could also set a property to disable tracing, but this is simpler.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Kernel is configured with a real AzureCognitiveSearchMemoryStore which is backed
|
||||
// by a mocked HttpPipeline. The HttpPipeline is mocked to return a mocked
|
||||
// HttpResponse whose body and status code are configured by the test.
|
||||
Kernel kernel;
|
||||
|
||||
//
|
||||
// A test may generate more than one HttpRequest. This function
|
||||
// can be set in the test to return the appropriate response body
|
||||
// for each request.
|
||||
//
|
||||
// A good place to set a breakpoint for debugging exceptions in
|
||||
// deserializing the response body is:
|
||||
// com.azure.search.documents.implementation.util.MappingUtils#exceptionMapper
|
||||
//
|
||||
Function<HttpRequest, String> responseBody;
|
||||
|
||||
//
|
||||
// A test may generate more than one HttpRequest. This function
|
||||
// can be set in the test to return the appropriate HTTP status
|
||||
// code for each request.
|
||||
//
|
||||
Function<HttpRequest, Integer> responseStatusCode;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
kernel = buildKernel(null);
|
||||
}
|
||||
|
||||
private Kernel buildKernel(
|
||||
@Nullable Function<SearchDocument, MemoryRecord> memoryRecordMapper) {
|
||||
SearchAsyncClient searchAsyncClient = Mockito.mock(SearchAsyncClient.class);
|
||||
SearchIndexAsyncClient searchIndexAsyncClient = Mockito.mock(SearchIndexAsyncClient.class);
|
||||
Response response = Mockito.mock(Response.class);
|
||||
AzureCognitiveSearchMemoryRecord azureCognitiveSearchMemoryRecord =
|
||||
Mockito.mock(AzureCognitiveSearchMemoryRecord.class);
|
||||
MemoryRecord memoryRecord = Mockito.mock(MemoryRecord.class);
|
||||
SearchPagedFlux searchPagedFlux = Mockito.mock(SearchPagedFlux.class);
|
||||
SearchResult searchResult = Mockito.mock(SearchResult.class);
|
||||
|
||||
Mockito.when(searchResult.getDocument(Mockito.any())).thenReturn(new SearchDocument());
|
||||
Mockito.when(searchIndexAsyncClient.getSearchAsyncClient(Mockito.any()))
|
||||
.thenReturn(searchAsyncClient);
|
||||
|
||||
Mockito.when(searchPagedFlux.filter(Mockito.any())).thenReturn(Flux.just(searchResult));
|
||||
|
||||
Mockito.when(
|
||||
searchAsyncClient.getDocumentWithResponse(
|
||||
Mockito.any(), Mockito.any(), Mockito.any()))
|
||||
.thenReturn(Mono.just(response));
|
||||
|
||||
Mockito.when(searchAsyncClient.search(Mockito.any(), Mockito.any()))
|
||||
.thenReturn(searchPagedFlux);
|
||||
Mockito.when(response.getValue()).thenReturn(azureCognitiveSearchMemoryRecord);
|
||||
Mockito.when(azureCognitiveSearchMemoryRecord.toMemoryRecord(Mockito.anyBoolean()))
|
||||
.thenReturn(memoryRecord);
|
||||
|
||||
EmbeddingGeneration<String> embeddingGeneration =
|
||||
new EmbeddingGeneration<String>() {
|
||||
/*
|
||||
new SearchIndexClientBuilder()
|
||||
.endpoint("https://fake-random-test-host/fake-path")
|
||||
.credential(new AzureKeyCredential(apiKey))
|
||||
.pipeline(pipeline)
|
||||
.httpLogOptions(
|
||||
new HttpLogOptions()
|
||||
.setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS))
|
||||
.buildAsyncClient();
|
||||
|
||||
*/
|
||||
|
||||
List<Embedding> embeddings = new ArrayList<>();
|
||||
|
||||
{
|
||||
embeddings.add(new Embedding(Arrays.asList(1.0f, 2.0f, 3.0f)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<Embedding>> generateEmbeddingsAsync(List<String> data) {
|
||||
return Mono.just(embeddings);
|
||||
}
|
||||
};
|
||||
|
||||
return SKBuilders.kernel()
|
||||
.withMemoryStorage(
|
||||
new AzureCognitiveSearchMemoryStore(
|
||||
searchIndexAsyncClient, memoryRecordMapper))
|
||||
.withDefaultAIService(embeddingGeneration)
|
||||
.build();
|
||||
}
|
||||
|
||||
private HttpPipeline setUpHttpPipeline() {
|
||||
|
||||
HttpPipeline pipeline = Mockito.mock(HttpPipeline.class);
|
||||
|
||||
Tracer tracer = new NoOpTracer();
|
||||
Mockito.when(pipeline.getTracer()).thenReturn(tracer);
|
||||
|
||||
Mockito.when(pipeline.send(Mockito.any(HttpRequest.class), Mockito.any(Context.class)))
|
||||
.thenAnswer(this::answerHttpRequest);
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
private Mono<HttpResponse> answerHttpRequest(InvocationOnMock invocation) {
|
||||
HttpRequest request = invocation.getArgument(0);
|
||||
|
||||
String query = request.getUrl().getQuery();
|
||||
int offset = query.indexOf("api-version");
|
||||
assertTrue(offset >= 0);
|
||||
// HttpResponse is based on this api-version
|
||||
assertEquals("api-version=2023-07-01-Preview", query.substring(offset));
|
||||
|
||||
// System.err.println(request.getHttpMethod() + " " + request.getUrl() + "\n\t" +
|
||||
// request.getHeaders());
|
||||
byte[] body = responseBody.apply(request).getBytes(StandardCharsets.UTF_8);
|
||||
int statusCode = responseStatusCode.apply(request);
|
||||
HttpHeaders httpHeaders =
|
||||
new HttpHeaders().add("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
|
||||
Mockito.when(httpResponse.getRequest()).thenReturn(request);
|
||||
Mockito.when(httpResponse.getHeaders()).thenReturn(httpHeaders);
|
||||
Mockito.when(httpResponse.getBodyAsByteArray()).thenReturn(Mono.just(body));
|
||||
Mockito.when(httpResponse.getStatusCode()).thenReturn(statusCode);
|
||||
return Mono.just(httpResponse);
|
||||
}
|
||||
|
||||
@Disabled("Pending https://github.com/microsoft/semantic-kernel/issues/1797")
|
||||
@Test
|
||||
void azureCognitiveSearchMemoryStoreShouldBeProperlyInitialized() {
|
||||
// Arrange
|
||||
responseBody = httpRequest -> "{\"value\": [{\"name\": \"fake-index\"}]}";
|
||||
responseStatusCode = httpResponse -> 200;
|
||||
|
||||
// Act
|
||||
// This call triggers a subsequent call to Azure Cognitive Search Memory store.
|
||||
List<String> collections = kernel.getMemory().getCollectionsAsync().block();
|
||||
|
||||
// Assert
|
||||
assertTrue(collections.contains("fake-index"));
|
||||
}
|
||||
|
||||
@Disabled("Pending https://github.com/microsoft/semantic-kernel/issues/1797")
|
||||
@Test
|
||||
void azureCognitiveSearchMemoryStoreCanSaveReferenceAsync() {
|
||||
// Arrange
|
||||
responseBody =
|
||||
httpRequest ->
|
||||
"{\"value\": ["
|
||||
+ " {"
|
||||
+ " \"key\": \"fake-key\","
|
||||
+ " \"status\": true,"
|
||||
+ " \"errorMessage\": null,"
|
||||
+ " \"statusCode\": 200"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
|
||||
responseStatusCode = httpRequest -> 200;
|
||||
|
||||
// Act
|
||||
String key =
|
||||
kernel.getMemory()
|
||||
.saveReferenceAsync(
|
||||
"fake-index",
|
||||
"fake-text",
|
||||
"fake-externalId",
|
||||
"fake-externalSourceName",
|
||||
"fake-description",
|
||||
"fake-additionalMetadata")
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertEquals("fake-key", key);
|
||||
}
|
||||
|
||||
@Disabled("Pending https://github.com/microsoft/semantic-kernel/issues/1797")
|
||||
@Test
|
||||
void azureCognitiveSearchMemoryStoreCanSaveInformationAsync() {
|
||||
// Arrange
|
||||
responseBody =
|
||||
httpRequest ->
|
||||
"{\"value\": ["
|
||||
+ " {"
|
||||
+ " \"key\": \"fake-key\","
|
||||
+ " \"status\": true,"
|
||||
+ " \"errorMessage\": null,"
|
||||
+ " \"statusCode\": 200"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
|
||||
responseStatusCode = httpRequest -> 200;
|
||||
|
||||
// Act
|
||||
String key =
|
||||
kernel.getMemory()
|
||||
.saveInformationAsync(
|
||||
"fake-index",
|
||||
"fake-text",
|
||||
"fake-externalId",
|
||||
"fake-description",
|
||||
"fake-additionalMetadata")
|
||||
.block();
|
||||
|
||||
// Assert
|
||||
assertEquals("fake-key", key);
|
||||
}
|
||||
|
||||
@Disabled("Pending https://github.com/microsoft/semantic-kernel/issues/1797")
|
||||
@Test
|
||||
void azureCognitiveSearchMemoryStoreGetAsync() {
|
||||
// Arrange
|
||||
String key = "fake-key";
|
||||
String base64EncodedKey =
|
||||
new String(
|
||||
Base64.getUrlEncoder().encode("fake-key".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
responseBody =
|
||||
httpRequest ->
|
||||
"{"
|
||||
+ " \"Id\": \""
|
||||
+ base64EncodedKey
|
||||
+ "\","
|
||||
+ " \"Text\": \"fake-text\","
|
||||
+ " \"Description\": \"fake-description\","
|
||||
+ " \"AdditionalMetadata\": \"fake-additionalmetadata\","
|
||||
+ " \"ExternalSourceName\": \"fake-externalsourcename\","
|
||||
+ " \"Reference\": false"
|
||||
+ "}";
|
||||
|
||||
responseStatusCode = httpRequest -> 200;
|
||||
|
||||
// Act
|
||||
MemoryQueryResult result = kernel.getMemory().getAsync("fake-index", key, false).block();
|
||||
|
||||
// Assert
|
||||
assertEquals(key, result.getMetadata().getId());
|
||||
}
|
||||
|
||||
@Disabled("Pending https://github.com/microsoft/semantic-kernel/issues/1797")
|
||||
@Test
|
||||
void azureCognitiveSearchMemoryStoreRemoveAsync() {
|
||||
// Arrange
|
||||
String key = "fake-key";
|
||||
String base64EncodedKey =
|
||||
new String(
|
||||
Base64.getUrlEncoder().encode("fake-key".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
responseBody =
|
||||
httpRequest ->
|
||||
"{\"value\": ["
|
||||
+ " {"
|
||||
+ " \"key\": \""
|
||||
+ base64EncodedKey
|
||||
+ "\","
|
||||
+ " \"status\": true,"
|
||||
+ " \"errorMessage\": null,"
|
||||
+ " \"statusCode\": 200"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
;
|
||||
|
||||
responseStatusCode = httpRequest -> 200;
|
||||
|
||||
// Act
|
||||
kernel.getMemory().removeAsync("fake-index", key).block();
|
||||
|
||||
// Assert
|
||||
}
|
||||
|
||||
@Disabled("Pending https://github.com/microsoft/semantic-kernel/issues/1797")
|
||||
@Test
|
||||
void azureCognitiveSearchMemoryStoreSearchAsync() {
|
||||
// Arrange
|
||||
String key = "fake-key";
|
||||
String base64EncodedKey =
|
||||
new String(
|
||||
Base64.getUrlEncoder().encode("fake-key".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
responseBody =
|
||||
httpRequest ->
|
||||
"{\"value\": ["
|
||||
+ "{"
|
||||
+ " \"@search.score\": 1,"
|
||||
+ " \"@search.highlights\": {"
|
||||
+ " \"Text\": [\"It was a dark and stormy night\"]"
|
||||
+ " },"
|
||||
+ " \"@search.features\": {"
|
||||
+ " \"Text\": {"
|
||||
+ " \"uniqueTokenMatches\": 1,"
|
||||
+ " \"similarityScore\": 0.8,"
|
||||
+ " \"termFrequency\": 0.99"
|
||||
+ " }"
|
||||
+ " },"
|
||||
+ " \"Id\": \""
|
||||
+ base64EncodedKey
|
||||
+ "\","
|
||||
+ " \"Text\": \"fake-text\","
|
||||
+ " \"Description\": \"fake-description\","
|
||||
+ " \"AdditionalMetadata\": \"fake-additionalmetadata\","
|
||||
+ " \"ExternalSourceName\": \"fake-externalsourcename\","
|
||||
+ " \"Reference\": false"
|
||||
+ "}"
|
||||
+ "]}";
|
||||
|
||||
responseStatusCode = httpRequest -> 200;
|
||||
|
||||
// Act
|
||||
List<MemoryQueryResult> results =
|
||||
kernel.getMemory().searchAsync("fake-index", "fake-query", 1, 0.5f, false).block();
|
||||
|
||||
// Assert
|
||||
assertEquals(1, results.size());
|
||||
assertEquals(key, results.get(0).getMetadata().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void azureCognitiveSearchMemoryStoreGetAsyncWithCustomMapping() {
|
||||
// Do not convert to lambda, it will break mockito
|
||||
Function<SearchDocument, MemoryRecord> transform =
|
||||
Mockito.spy(
|
||||
new Function<SearchDocument, MemoryRecord>() {
|
||||
@Override
|
||||
public MemoryRecord apply(SearchDocument searchDocument) {
|
||||
return new MemoryRecord(
|
||||
new MemoryRecordMetadata(
|
||||
false,
|
||||
"an-id",
|
||||
"fake-text",
|
||||
"fake-description",
|
||||
"fake-additionalmetadata",
|
||||
"fake-externalsourcename"),
|
||||
new Embedding(Arrays.asList(1.0f, 2.0f, 3.0f)),
|
||||
"an-id",
|
||||
null);
|
||||
}
|
||||
});
|
||||
|
||||
kernel = buildKernel(transform);
|
||||
// Arrange
|
||||
String key = "fake-key";
|
||||
String base64EncodedKey =
|
||||
new String(
|
||||
Base64.getUrlEncoder().encode("fake-key".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
responseBody =
|
||||
httpRequest ->
|
||||
"{"
|
||||
+ " \"Id\": \""
|
||||
+ base64EncodedKey
|
||||
+ "\","
|
||||
+ " \"Text\": \"fake-text\","
|
||||
+ " \"Description\": \"fake-description\","
|
||||
+ " \"AdditionalMetadata\": \"fake-additionalmetadata\","
|
||||
+ " \"ExternalSourceName\": \"fake-externalsourcename\","
|
||||
+ " \"Reference\": false"
|
||||
+ "}";
|
||||
|
||||
responseStatusCode = httpRequest -> 200;
|
||||
|
||||
// Act
|
||||
kernel.getMemory().searchAsync("fake-index", "a-query", 1, 0.0f, false).block();
|
||||
|
||||
Mockito.verify(transform, Mockito.atLeastOnce()).apply(Mockito.any());
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-connectors-memory-jdbc</artifactId>
|
||||
<name>Semantic Kernel JDBC Memory Connector</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="azure" name="Azure">
|
||||
<configuration>
|
||||
<option name="dotAzurePath" value="$MODULE_DIR$/.azure" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
</module>
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import com.microsoft.semantickernel.memory.DataEntryBase;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/** Represents an entry in the Semantic Kernel Memory Table. */
|
||||
public class DatabaseEntry extends DataEntryBase {
|
||||
private final String embedding; // JSON with the embedding information
|
||||
private final String metadata; // JSON with the metadata associated
|
||||
|
||||
/**
|
||||
* Creates a new instance of DatabaseEntry.
|
||||
*
|
||||
* @param key The key identifying the entry.
|
||||
* @param metadata The metadata associated.
|
||||
* @param embedding The embedding information associated.
|
||||
* @param timestamp The timestamp indicating when the entry was created or modified.
|
||||
*/
|
||||
public DatabaseEntry(String key, String metadata, String embedding, ZonedDateTime timestamp) {
|
||||
super(key, timestamp);
|
||||
this.metadata = metadata;
|
||||
this.embedding = embedding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metadata associated with the entry.
|
||||
*
|
||||
* @return The metadata associated with the entry.
|
||||
*/
|
||||
public String getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the embedding information associated with the entry.
|
||||
*
|
||||
* @return The embedding information associated with the entry.
|
||||
*/
|
||||
public String getEmbedding() {
|
||||
return embedding;
|
||||
}
|
||||
}
|
||||
-495
@@ -1,495 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import com.microsoft.semantickernel.memory.MemoryException;
|
||||
import com.microsoft.semantickernel.memory.MemoryException.ErrorCodes;
|
||||
import java.io.Closeable;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class JDBCConnector implements SQLConnector, Closeable {
|
||||
protected final Connection connection;
|
||||
|
||||
public JDBCConnector(Connection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
// Convenience method to format a ZonedDateTime in a format acceptable to SQL
|
||||
protected static String formatDatetime(@Nullable ZonedDateTime datetime) {
|
||||
if (datetime == null) return "";
|
||||
return datetime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
}
|
||||
|
||||
// Convenience method to parse a SQL datetime string into a ZonedDateTime
|
||||
protected static ZonedDateTime parseDatetime(@Nullable String datetime) {
|
||||
if (datetime == null || datetime.isEmpty()) return null;
|
||||
return ZonedDateTime.parse(datetime, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
}
|
||||
|
||||
protected static final String COLLECTIONS_TABLE_NAME = "SKCollectionTable";
|
||||
protected static final String TABLE_NAME = "SKMemoryTable";
|
||||
protected static final String INDEX_NAME = "SKMemoryIndex";
|
||||
|
||||
/**
|
||||
* Returns the name of the table that stores the collection names.
|
||||
*
|
||||
* @return The name of the table that stores the collection names.
|
||||
*/
|
||||
protected static String DEFAULT_COLLECTIONS_TABLE_NAME() {
|
||||
return COLLECTIONS_TABLE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the Semantic Kernel table.
|
||||
*
|
||||
* @return The name of the Semantic Kernel table.
|
||||
*/
|
||||
protected static String DEFAULT_TABLE_NAME() {
|
||||
return TABLE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the index on the Semantic Kernel table.
|
||||
*
|
||||
* @return The name of the index on the Semantic Kernel table.
|
||||
*/
|
||||
protected static String DEFAULT_INDEX_NAME() {
|
||||
return INDEX_NAME;
|
||||
}
|
||||
|
||||
public Mono<Void> createTableAsync() {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String createCollectionKeyTable =
|
||||
"CREATE TABLE IF NOT EXISTS "
|
||||
+ COLLECTIONS_TABLE_NAME
|
||||
+ " ("
|
||||
+ "id TEXT PRIMARY KEY"
|
||||
+ " )";
|
||||
|
||||
String createSKMemoryTable =
|
||||
"CREATE TABLE IF NOT EXISTS "
|
||||
+ TABLE_NAME
|
||||
+ " ("
|
||||
+ "collection TEXT NOT NULL, "
|
||||
+ "key TEXT NOT NULL, "
|
||||
+ "metadata TEXT, "
|
||||
+ "embedding TEXT, "
|
||||
+ "timestamp TEXT, "
|
||||
+ "PRIMARY KEY (collection, key), "
|
||||
+ "FOREIGN KEY (collection) REFERENCES "
|
||||
+ COLLECTIONS_TABLE_NAME
|
||||
+ "(id)"
|
||||
+ " )";
|
||||
|
||||
String createIndex =
|
||||
"CREATE INDEX IF NOT EXISTS "
|
||||
+ INDEX_NAME
|
||||
+ " ON "
|
||||
+ TABLE_NAME
|
||||
+ "(collection)";
|
||||
|
||||
try (Statement statement = this.connection.createStatement()) {
|
||||
statement.addBatch(createCollectionKeyTable);
|
||||
statement.addBatch(createSKMemoryTable);
|
||||
statement.addBatch(createIndex);
|
||||
statement.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"CREATE TABLE\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
public Mono<Void> createCollectionAsync(String collectionName) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"INSERT INTO " + COLLECTIONS_TABLE_NAME + " (id) VALUES (?)";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"INSERT INTO\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
public Mono<String> upsertAsync(
|
||||
String collection,
|
||||
String key,
|
||||
String metadata,
|
||||
String embedding,
|
||||
ZonedDateTime timestamp) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"INSERT OR REPLACE INTO "
|
||||
+ TABLE_NAME
|
||||
+ " (collection, key, metadata, embedding, timestamp)"
|
||||
+ " VALUES (?, ?, ?, ?, ?)";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collection);
|
||||
statement.setString(2, key);
|
||||
statement.setString(3, metadata != null ? metadata : "");
|
||||
statement.setString(4, embedding != null ? embedding : "");
|
||||
statement.setString(5, formatDatetime(timestamp));
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"INSERT OR REPLACE INTO\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.thenReturn(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<String>> upsertBatchAsync(
|
||||
String collection, Collection<DatabaseEntry> records) {
|
||||
Collection<String> keys = new ArrayList<>();
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"INSERT OR REPLACE INTO "
|
||||
+ TABLE_NAME
|
||||
+ " (collection, key, metadata, embedding, timestamp)"
|
||||
+ " VALUES (?, ?, ?, ?, ?)";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
for (DatabaseEntry entry : records) {
|
||||
statement.setString(1, collection);
|
||||
statement.setString(2, entry.getKey());
|
||||
statement.setString(
|
||||
3,
|
||||
entry.getMetadata() != null ? entry.getMetadata() : "");
|
||||
statement.setString(
|
||||
4,
|
||||
entry.getEmbedding() != null
|
||||
? entry.getEmbedding()
|
||||
: "");
|
||||
statement.setString(5, formatDatetime(entry.getTimestamp()));
|
||||
statement.addBatch();
|
||||
keys.add(entry.getKey());
|
||||
}
|
||||
|
||||
statement.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"INSERT OR REPLACE INTO\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.thenReturn(keys);
|
||||
}
|
||||
|
||||
public Mono<Boolean> doesCollectionExistsAsync(String collectionName) {
|
||||
return Mono.fromCallable(() -> doesCollectionExists(collectionName))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private boolean doesCollectionExists(String collectionName) throws SQLException {
|
||||
String query = "SELECT id FROM " + COLLECTIONS_TABLE_NAME + " WHERE id = ?";
|
||||
try (PreparedStatement statement = this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
return resultSet.next();
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<List<String>> getCollectionsAsync() {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
List<String> collections = new ArrayList<>();
|
||||
String query = "SELECT id FROM " + COLLECTIONS_TABLE_NAME;
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
String collection = resultSet.getString("id");
|
||||
collections.add(collection);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(
|
||||
new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"SELECT\" failed",
|
||||
e));
|
||||
}
|
||||
return Mono.just(collections);
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
public Mono<List<DatabaseEntry>> readAllAsync(String collectionName) {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
List<DatabaseEntry> entries = new ArrayList<>();
|
||||
String query = "SELECT * FROM " + TABLE_NAME + " WHERE collection = ?";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
String key = resultSet.getString("key");
|
||||
String metadata = resultSet.getString("metadata");
|
||||
String embedding = resultSet.getString("embedding");
|
||||
String timestamp = resultSet.getString("timestamp");
|
||||
ZonedDateTime zonedDateTime = parseDatetime(timestamp);
|
||||
entries.add(
|
||||
new DatabaseEntry(
|
||||
key, metadata, embedding, zonedDateTime));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(
|
||||
new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"SELECT * FROM\" failed",
|
||||
e));
|
||||
}
|
||||
return Mono.just(entries);
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
public Mono<DatabaseEntry> readAsync(String collectionName, String key) {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
String query =
|
||||
"SELECT * FROM "
|
||||
+ TABLE_NAME
|
||||
+ " WHERE collection = ?"
|
||||
+ " AND key = ?";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.setString(2, key != null && !key.isEmpty() ? key : null);
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
String metadata = resultSet.getString("metadata");
|
||||
String embedding = resultSet.getString("embedding");
|
||||
String timestamp = resultSet.getString("timestamp");
|
||||
ZonedDateTime zonedDateTime = parseDatetime(timestamp);
|
||||
return Mono.just(
|
||||
new DatabaseEntry(
|
||||
key, metadata, embedding, zonedDateTime));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(
|
||||
new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"SELECT * FROM\" failed",
|
||||
e));
|
||||
}
|
||||
return Mono.empty();
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
protected enum BatchOperation {
|
||||
SELECT,
|
||||
DELETE
|
||||
}
|
||||
|
||||
protected String batchQuery(BatchOperation operation, Collection<String> keys) {
|
||||
String queryPrefix;
|
||||
switch (operation) {
|
||||
case SELECT:
|
||||
queryPrefix = "SELECT * FROM " + TABLE_NAME + " WHERE collection = ?";
|
||||
break;
|
||||
case DELETE:
|
||||
queryPrefix = "DELETE FROM " + TABLE_NAME + " WHERE collection = ?";
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid batch operation");
|
||||
}
|
||||
StringBuilder queryBuilder = new StringBuilder(queryPrefix);
|
||||
queryBuilder.append(" AND key IN (");
|
||||
|
||||
// Add placeholders for each key
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
queryBuilder.append("?");
|
||||
if (i < keys.size() - 1) {
|
||||
queryBuilder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
queryBuilder.append(")");
|
||||
return queryBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<DatabaseEntry>> readBatchAsync(
|
||||
String collectionName, Collection<String> keys) {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
Collection<DatabaseEntry> entries = new ArrayList<>();
|
||||
String query = batchQuery(BatchOperation.SELECT, keys);
|
||||
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
int index = 2;
|
||||
for (String key : keys) {
|
||||
statement.setString(index++, key);
|
||||
}
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
|
||||
while (resultSet.next()) {
|
||||
String key = resultSet.getString("key");
|
||||
String metadata = resultSet.getString("metadata");
|
||||
String embedding = resultSet.getString("embedding");
|
||||
String timestamp = resultSet.getString("timestamp");
|
||||
ZonedDateTime zonedDateTime = parseDatetime(timestamp);
|
||||
entries.add(
|
||||
new DatabaseEntry(
|
||||
key, metadata, embedding, zonedDateTime));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(
|
||||
new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"SELECT * FROM\" failed",
|
||||
e));
|
||||
}
|
||||
return Mono.just(entries);
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
public Mono<Void> deleteCollectionAsync(String collectionName) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query1 = "DELETE FROM " + TABLE_NAME + " WHERE collection = ?";
|
||||
String query2 =
|
||||
"DELETE FROM " + COLLECTIONS_TABLE_NAME + " WHERE id = ?";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query1);
|
||||
PreparedStatement statement2 =
|
||||
this.connection.prepareStatement(query2)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.executeUpdate();
|
||||
statement2.setString(1, collectionName);
|
||||
if (statement2.executeUpdate() == 0) {
|
||||
throw new MemoryException(
|
||||
ErrorCodes.ATTEMPTED_TO_ACCESS_NONEXISTENT_COLLECTION);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"DELETE FROM\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
public Mono<Void> deleteAsync(String collectionName, String key) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"DELETE FROM "
|
||||
+ TABLE_NAME
|
||||
+ " WHERE collection = ?"
|
||||
+ " AND key = ?";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.setString(2, key != null && !key.isEmpty() ? key : null);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"DELETE FROM\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
public Mono<Void> deleteBatchAsync(String collectionName, Collection<String> keys) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query = batchQuery(BatchOperation.DELETE, keys);
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
int index = 2;
|
||||
for (String key : keys) {
|
||||
statement.setString(index++, key);
|
||||
}
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"DELETE FROM\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
public Mono<Void> deleteEmptyAsync(String collectionName) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"DELETE FROM "
|
||||
+ TABLE_NAME
|
||||
+ " WHERE collection = ?"
|
||||
+ " AND key is NULL";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"DELETE FROM\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
this.connection.close();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"Database access error while closing connection",
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
-320
@@ -1,320 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.memory.MemoryException;
|
||||
import com.microsoft.semantickernel.memory.MemoryException.ErrorCodes;
|
||||
import com.microsoft.semantickernel.memory.MemoryRecord;
|
||||
import java.sql.Connection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nonnull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
public class JDBCMemoryStore implements SQLMemoryStore {
|
||||
protected final SQLConnector dbConnector;
|
||||
|
||||
protected JDBCMemoryStore(SQLConnector connector) {
|
||||
this.dbConnector = connector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> createCollectionAsync(@Nonnull String collectionName) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
return this.dbConnector.createCollectionAsync(collectionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<String>> getCollectionsAsync() {
|
||||
return this.dbConnector.getCollectionsAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> doesCollectionExistAsync(@Nonnull String collectionName) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
return this.dbConnector.doesCollectionExistsAsync(collectionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteCollectionAsync(@Nonnull String collectionName) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
return this.dbConnector.deleteCollectionAsync(collectionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> upsertAsync(@Nonnull String collectionName, @Nonnull MemoryRecord record) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(record);
|
||||
return doesCollectionExistAsync(collectionName)
|
||||
.handle(
|
||||
(exists, sink) -> {
|
||||
if (!exists) {
|
||||
sink.error(
|
||||
new MemoryException(
|
||||
ErrorCodes
|
||||
.ATTEMPTED_TO_ACCESS_NONEXISTENT_COLLECTION,
|
||||
collectionName));
|
||||
return;
|
||||
}
|
||||
sink.next(exists);
|
||||
})
|
||||
.then(internalUpsertAsync(collectionName, record));
|
||||
}
|
||||
|
||||
protected DatabaseEntry memoryRecordToDatabaseEntry(MemoryRecord record) {
|
||||
try {
|
||||
return new DatabaseEntry(
|
||||
record.getMetadata().getId(),
|
||||
record.getSerializedMetadata(),
|
||||
record.getSerializedEmbedding(),
|
||||
record.getTimestamp());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected MemoryRecord databaseEntryToMemoryRecord(DatabaseEntry entry, boolean withEmbedding) {
|
||||
try {
|
||||
Embedding embedding =
|
||||
withEmbedding
|
||||
? new ObjectMapper().readValue(entry.getEmbedding(), Embedding.class)
|
||||
: Embedding.empty();
|
||||
return MemoryRecord.fromJsonMetadata(
|
||||
entry.getMetadata(), embedding, entry.getKey(), entry.getTimestamp());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"Error deserializing database entry",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<String> internalUpsertAsync(String collectionName, MemoryRecord record) {
|
||||
try {
|
||||
return this.dbConnector.upsertAsync(
|
||||
collectionName,
|
||||
record.getMetadata().getId(),
|
||||
record.getSerializedMetadata(),
|
||||
record.getSerializedEmbedding(),
|
||||
record.getTimestamp());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"Error serializing MemoryRecord",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<String>> upsertBatchAsync(
|
||||
@Nonnull String collectionName, @Nonnull Collection<MemoryRecord> records) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(records);
|
||||
return doesCollectionExistAsync(collectionName)
|
||||
.handle(
|
||||
(exists, sink) -> {
|
||||
if (!exists) {
|
||||
sink.error(
|
||||
new MemoryException(
|
||||
ErrorCodes
|
||||
.ATTEMPTED_TO_ACCESS_NONEXISTENT_COLLECTION,
|
||||
collectionName));
|
||||
return;
|
||||
}
|
||||
sink.next(exists);
|
||||
})
|
||||
.then(
|
||||
dbConnector.upsertBatchAsync(
|
||||
collectionName,
|
||||
records.stream()
|
||||
.map(this::memoryRecordToDatabaseEntry)
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MemoryRecord> getAsync(
|
||||
@Nonnull String collectionName, @Nonnull String key, boolean withEmbedding) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(key);
|
||||
return this.internalGetAsync(collectionName, key, withEmbedding);
|
||||
}
|
||||
|
||||
private Mono<MemoryRecord> internalGetAsync(
|
||||
String collectionName, String key, boolean withEmbedding) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(key);
|
||||
Mono<DatabaseEntry> entry = this.dbConnector.readAsync(collectionName, key);
|
||||
|
||||
return entry.hasElement()
|
||||
.flatMap(
|
||||
hasElement -> {
|
||||
if (!hasElement) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
return entry.map(
|
||||
databaseEntry ->
|
||||
databaseEntryToMemoryRecord(
|
||||
databaseEntry, withEmbedding));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<MemoryRecord>> getBatchAsync(
|
||||
@Nonnull String collectionName,
|
||||
@Nonnull Collection<String> keys,
|
||||
boolean withEmbeddings) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(keys);
|
||||
return this.dbConnector
|
||||
.readBatchAsync(collectionName, keys)
|
||||
.flatMap(
|
||||
databaseEntries -> {
|
||||
List<MemoryRecord> records = new ArrayList<>();
|
||||
for (DatabaseEntry entry : databaseEntries) {
|
||||
records.add(databaseEntryToMemoryRecord(entry, withEmbeddings));
|
||||
}
|
||||
return Mono.just(records);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeAsync(@Nonnull String collectionName, @Nonnull String key) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(key);
|
||||
return this.dbConnector.deleteAsync(collectionName, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeBatchAsync(
|
||||
@Nonnull String collectionName, @Nonnull Collection<String> keys) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(keys);
|
||||
return this.dbConnector.deleteBatchAsync(collectionName, keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<Tuple2<MemoryRecord, Float>>> getNearestMatchesAsync(
|
||||
@Nonnull String collectionName,
|
||||
@Nonnull Embedding embedding,
|
||||
int limit,
|
||||
float minRelevanceScore,
|
||||
boolean withEmbeddings) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(embedding);
|
||||
Mono<List<DatabaseEntry>> entries = this.dbConnector.readAllAsync(collectionName);
|
||||
|
||||
return entries.flatMap(
|
||||
databaseEntries -> {
|
||||
List<Tuple2<MemoryRecord, Float>> nearestMatches = new ArrayList<>();
|
||||
for (DatabaseEntry entry : databaseEntries) {
|
||||
if (entry.getEmbedding() == null || entry.getEmbedding().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Embedding recordEmbedding =
|
||||
new ObjectMapper()
|
||||
.readValue(entry.getEmbedding(), Embedding.class);
|
||||
float similarity = embedding.cosineSimilarity(recordEmbedding);
|
||||
if (similarity >= (float) minRelevanceScore) {
|
||||
MemoryRecord record =
|
||||
MemoryRecord.fromJsonMetadata(
|
||||
entry.getMetadata(),
|
||||
withEmbeddings ? recordEmbedding : null,
|
||||
entry.getKey(),
|
||||
entry.getTimestamp());
|
||||
nearestMatches.add(Tuples.of(record, similarity));
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"Error deserializing database entry",
|
||||
e);
|
||||
}
|
||||
}
|
||||
List<Tuple2<MemoryRecord, Float>> results =
|
||||
nearestMatches.stream()
|
||||
.sorted(
|
||||
Comparator.comparing(
|
||||
Tuple2::getT2, (a, b) -> Float.compare(b, a)))
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Mono.just(results);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Tuple2<MemoryRecord, Float>> getNearestMatchAsync(
|
||||
@Nonnull String collectionName,
|
||||
@Nonnull Embedding embedding,
|
||||
float minRelevanceScore,
|
||||
boolean withEmbedding) {
|
||||
Objects.requireNonNull(collectionName);
|
||||
Objects.requireNonNull(embedding);
|
||||
return getNearestMatchesAsync(
|
||||
collectionName, embedding, 1, minRelevanceScore, withEmbedding)
|
||||
.flatMap(
|
||||
nearestMatches -> {
|
||||
if (nearestMatches.isEmpty()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.just(nearestMatches.iterator().next());
|
||||
});
|
||||
}
|
||||
|
||||
/** Builds a JDBCMemoryStore. */
|
||||
public static class Builder implements SQLMemoryStore.Builder<JDBCMemoryStore> {
|
||||
private Connection connection;
|
||||
|
||||
/**
|
||||
* Builds and returns a JDBCMemoryStore instance with the specified database connection. The
|
||||
* build process will connect to the database and create the required tables.
|
||||
*
|
||||
* @return A JDBCMemoryStore instance configured with the provided database connection.
|
||||
* @deprecated Use {@link #buildAsync()} instead.
|
||||
*/
|
||||
@Override
|
||||
@Deprecated
|
||||
public JDBCMemoryStore build() {
|
||||
return this.buildAsync().block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously builds and returns a JDBCMemoryStore instance with the specified database
|
||||
* connection.
|
||||
*
|
||||
* @return A Mono with a JDBCMemoryStore instance configured with the provided database
|
||||
* connection.
|
||||
*/
|
||||
@Override
|
||||
@CheckReturnValue
|
||||
public Mono<JDBCMemoryStore> buildAsync() {
|
||||
JDBCConnector connector = new JDBCConnector(connection);
|
||||
JDBCMemoryStore memoryStore = new JDBCMemoryStore(connector);
|
||||
return connector.createTableAsync().thenReturn(memoryStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the database connection to be used by the memory store being built.
|
||||
*
|
||||
* @param connection The Connection object representing the database connection.
|
||||
* @return The updated Builder instance to continue the building process for a
|
||||
* JDBCMemoryStore.
|
||||
*/
|
||||
@Override
|
||||
public Builder withConnection(Connection connection) {
|
||||
this.connection = connection;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface SQLConnector {
|
||||
|
||||
/**
|
||||
* Asynchronously creates a table in the database.
|
||||
*
|
||||
* @return A Mono representing the completion of the table creation operation.
|
||||
*/
|
||||
Mono<Void> createTableAsync();
|
||||
|
||||
/**
|
||||
* Asynchronously creates a collection within the database.
|
||||
*
|
||||
* @param collectionName The name of the collection to be created.
|
||||
* @return A Mono representing the completion of the collection creation operation.
|
||||
*/
|
||||
Mono<Void> createCollectionAsync(String collectionName);
|
||||
|
||||
/**
|
||||
* Asynchronously inserts or updates an entry.
|
||||
*
|
||||
* @param collection The name of the collection to insert the entry into.
|
||||
* @param key The key identifying the entry.
|
||||
* @param metadata The metadata associated with the entry.
|
||||
* @param embedding The embedding data associated with the entry.
|
||||
* @param timestamp The timestamp of the insertion.
|
||||
* @return A Mono representing the completion of the insertion operation.
|
||||
*/
|
||||
Mono<String> upsertAsync(
|
||||
String collection,
|
||||
String key,
|
||||
String metadata,
|
||||
String embedding,
|
||||
ZonedDateTime timestamp);
|
||||
|
||||
/**
|
||||
* Asynchronously inserts or updates a batch of entries.
|
||||
*
|
||||
* @param records
|
||||
* @return A Mono emitting a list of keys identifying the entries.
|
||||
*/
|
||||
Mono<Collection<String>> upsertBatchAsync(String collection, Collection<DatabaseEntry> records);
|
||||
|
||||
/**
|
||||
* Asynchronously checks if a collection exists.
|
||||
*
|
||||
* @param collectionName The name of the collection to check.
|
||||
* @return A Mono emitting a boolean value indicating whether the collection exists.
|
||||
*/
|
||||
Mono<Boolean> doesCollectionExistsAsync(String collectionName);
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves a list of collection names.
|
||||
*
|
||||
* @return A Mono emitting a list of collection names.
|
||||
*/
|
||||
Mono<List<String>> getCollectionsAsync();
|
||||
|
||||
/**
|
||||
* Asynchronously reads all entries within a collection.
|
||||
*
|
||||
* @param collectionName The name of the collection to read from.
|
||||
* @return A Mono emitting a list of DatabaseEntry objects representing the entries.
|
||||
*/
|
||||
Mono<List<DatabaseEntry>> readAllAsync(String collectionName);
|
||||
|
||||
/**
|
||||
* Asynchronously reads a specific entry within a collection.
|
||||
*
|
||||
* @param collectionName The name of the collection to read from.
|
||||
* @param key The key identifying the entry to read.
|
||||
* @return A Mono emitting a DatabaseEntry object representing the entry.
|
||||
*/
|
||||
Mono<DatabaseEntry> readAsync(String collectionName, String key);
|
||||
|
||||
/**
|
||||
* Asynchronously reads a batch of entries within a collection.
|
||||
*
|
||||
* @param collectionName The name of the collection to read from.
|
||||
* @param keys The keys identifying the entries to read.
|
||||
* @return A Mono emitting a list of DatabaseEntry objects representing the entries.
|
||||
*/
|
||||
Mono<Collection<DatabaseEntry>> readBatchAsync(String collectionName, Collection<String> keys);
|
||||
|
||||
/**
|
||||
* Asynchronously deletes a collection.
|
||||
*
|
||||
* @param collectionName The name of the collection to delete.
|
||||
* @return A Mono representing the completion of the collection deletion operation.
|
||||
*/
|
||||
Mono<Void> deleteCollectionAsync(String collectionName);
|
||||
|
||||
/**
|
||||
* Asynchronously deletes an entry within a collection.
|
||||
*
|
||||
* @param collectionName The name of the collection to delete the entry from.
|
||||
* @param key The key identifying the entry to delete.
|
||||
* @return A Mono representing the completion of the entry deletion operation.
|
||||
*/
|
||||
Mono<Void> deleteAsync(String collectionName, String key);
|
||||
|
||||
/**
|
||||
* Asynchronously deletes a batch of entries within a collection.
|
||||
*
|
||||
* @param collectionName The name of the collection to delete the entries from.
|
||||
* @param keys The keys identifying the entries to delete.
|
||||
* @return A Mono representing the completion of the entry deletion operation.
|
||||
*/
|
||||
Mono<Void> deleteBatchAsync(String collectionName, Collection<String> keys);
|
||||
|
||||
/**
|
||||
* Asynchronously deletes all empty entries within a collection.
|
||||
*
|
||||
* @param collectionName The name of the collection to delete empty entries from.
|
||||
* @return A Mono representing the completion of the empty entry deletion operation.
|
||||
*/
|
||||
Mono<Void> deleteEmptyAsync(String collectionName);
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import com.microsoft.semantickernel.SKException;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Exception thrown by the SQL connector. */
|
||||
public class SQLConnectorException extends SKException {
|
||||
|
||||
private final ErrorCodes errorCode;
|
||||
|
||||
/**
|
||||
* Create an exception with a message
|
||||
*
|
||||
* @param errorCode The error code
|
||||
*/
|
||||
public SQLConnectorException(@Nonnull ErrorCodes errorCode) {
|
||||
this(errorCode, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an exception with a message
|
||||
*
|
||||
* @param message a description of the cause of the exception
|
||||
*/
|
||||
public SQLConnectorException(@Nonnull ErrorCodes errorCode, @Nullable String message) {
|
||||
this(errorCode, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an exception with a message and a cause
|
||||
*
|
||||
* @param errorCode the error code
|
||||
* @param message a description of the cause of the exception
|
||||
* @param cause the cause of the exception
|
||||
*/
|
||||
public SQLConnectorException(
|
||||
@Nonnull ErrorCodes errorCode, @Nullable String message, @Nullable Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ErrorCodes getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public enum ErrorCodes {
|
||||
SQL_ERROR("SQL error");
|
||||
|
||||
final String message;
|
||||
|
||||
ErrorCodes(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import com.microsoft.semantickernel.memory.MemoryStore;
|
||||
import java.sql.Connection;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface SQLMemoryStore extends MemoryStore {
|
||||
interface Builder<T extends SQLMemoryStore> extends MemoryStore.Builder<T> {
|
||||
/**
|
||||
* Asynchronously builds the SQLMemoryStore.
|
||||
*
|
||||
* @return A Mono with the SQLMemoryStore.
|
||||
*/
|
||||
Mono<T> buildAsync();
|
||||
|
||||
/**
|
||||
* Sets the database connection to be used by the SQL memory store.
|
||||
*
|
||||
* @param connection The Connection object representing the database connection.
|
||||
* @return The updated SQLMemoryStoreBuilder instance.
|
||||
*/
|
||||
Builder<T> withConnection(Connection connection);
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class JDBCConnectorTest {
|
||||
@Test
|
||||
void testBatchQuery() {
|
||||
Collection<String> keys = new ArrayList<>();
|
||||
keys.add("key1");
|
||||
keys.add("key2");
|
||||
keys.add("key3");
|
||||
|
||||
JDBCConnector connector = new JDBCConnector(null);
|
||||
String read = connector.batchQuery(JDBCConnector.BatchOperation.SELECT, keys);
|
||||
String delete = connector.batchQuery(JDBCConnector.BatchOperation.DELETE, keys);
|
||||
|
||||
assertNotNull(read);
|
||||
assertNotNull(delete);
|
||||
assertEquals(
|
||||
"SELECT * FROM "
|
||||
+ JDBCConnector.TABLE_NAME
|
||||
+ " WHERE collection = ? AND key IN (?,?,?)",
|
||||
read);
|
||||
assertEquals(
|
||||
"DELETE FROM "
|
||||
+ JDBCConnector.TABLE_NAME
|
||||
+ " WHERE collection = ? AND key IN (?,?,?)",
|
||||
delete);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-connectors-memory-mysql</artifactId>
|
||||
<name>Semantic Kernel MySQL Memory Connector</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
-375
@@ -1,375 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.mysql;
|
||||
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.DatabaseEntry;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.JDBCConnector;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLConnector;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLConnectorException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class MySQLConnector extends JDBCConnector implements SQLConnector {
|
||||
public MySQLConnector(Connection connection) {
|
||||
super(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> createTableAsync() {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
String createCollectionKeyTable =
|
||||
"CREATE TABLE IF NOT EXISTS "
|
||||
+ COLLECTIONS_TABLE_NAME
|
||||
+ " (id VARCHAR(255) PRIMARY KEY);";
|
||||
|
||||
String createSKMemoryTable =
|
||||
"CREATE TABLE IF NOT EXISTS "
|
||||
+ TABLE_NAME
|
||||
+ " ("
|
||||
+ "collection VARCHAR(255) NOT NULL, "
|
||||
+ "id VARCHAR(255) NOT NULL, "
|
||||
+ "metadata TEXT, "
|
||||
+ "embedding TEXT, "
|
||||
+ "timestamp TEXT, "
|
||||
+ "PRIMARY KEY (collection, id), "
|
||||
+ "FOREIGN KEY (collection) REFERENCES "
|
||||
+ COLLECTIONS_TABLE_NAME
|
||||
+ "(id));";
|
||||
|
||||
statement.addBatch(createCollectionKeyTable);
|
||||
statement.addBatch(createSKMemoryTable);
|
||||
|
||||
boolean indexExists = doesIndexExist();
|
||||
|
||||
if (!indexExists) {
|
||||
String createIndex =
|
||||
"CREATE INDEX "
|
||||
+ INDEX_NAME
|
||||
+ " ON "
|
||||
+ TABLE_NAME
|
||||
+ "(collection)";
|
||||
statement.addBatch(createIndex);
|
||||
}
|
||||
|
||||
statement.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"CREATE TABLE\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
private boolean doesIndexExist() throws SQLException {
|
||||
DatabaseMetaData meta = connection.getMetaData();
|
||||
try (ResultSet indexes = meta.getIndexInfo(null, null, TABLE_NAME, false, false)) {
|
||||
while (indexes.next()) {
|
||||
String existingIndexName = indexes.getString("INDEX_NAME");
|
||||
if (existingIndexName != null && existingIndexName.equalsIgnoreCase(INDEX_NAME)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> createCollectionAsync(String collectionName) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"INSERT INTO " + COLLECTIONS_TABLE_NAME + " (id) VALUES (?)";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"INSERT INTO\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> upsertAsync(
|
||||
String collection,
|
||||
String key,
|
||||
String metadata,
|
||||
String embedding,
|
||||
ZonedDateTime timestamp) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"INSERT INTO "
|
||||
+ TABLE_NAME
|
||||
+ " (collection, id, metadata, embedding, timestamp)"
|
||||
+ " VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE"
|
||||
+ " metadata = VALUES(metadata), embedding ="
|
||||
+ " VALUES(embedding), timestamp = VALUES(timestamp)";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
String metadataString = metadata != null ? metadata : "";
|
||||
String embeddingString = embedding != null ? embedding : "";
|
||||
String timestampString = formatDatetime(timestamp);
|
||||
statement.setString(1, collection);
|
||||
statement.setString(2, key);
|
||||
statement.setString(3, metadataString);
|
||||
statement.setString(4, embeddingString);
|
||||
statement.setString(5, timestampString);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"INSERT INTO\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.thenReturn(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<String>> upsertBatchAsync(
|
||||
String collection, Collection<DatabaseEntry> records) {
|
||||
Collection<String> keys = new ArrayList<>();
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"INSERT INTO "
|
||||
+ TABLE_NAME
|
||||
+ " (collection, id, metadata, embedding, timestamp)"
|
||||
+ " VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE"
|
||||
+ " metadata = VALUES(metadata), embedding ="
|
||||
+ " VALUES(embedding), timestamp = VALUES(timestamp)";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
for (DatabaseEntry entry : records) {
|
||||
String metadataString =
|
||||
entry.getMetadata() != null ? entry.getMetadata() : "";
|
||||
String embeddingString =
|
||||
entry.getEmbedding() != null
|
||||
? entry.getEmbedding()
|
||||
: "";
|
||||
String timestampString = formatDatetime(entry.getTimestamp());
|
||||
statement.setString(1, collection);
|
||||
statement.setString(2, entry.getKey());
|
||||
statement.setString(3, metadataString);
|
||||
statement.setString(4, embeddingString);
|
||||
statement.setString(5, timestampString);
|
||||
statement.addBatch();
|
||||
keys.add(entry.getKey());
|
||||
}
|
||||
|
||||
statement.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"INSERT INTO\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.thenReturn(keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<DatabaseEntry>> readAllAsync(String collectionName) {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
List<DatabaseEntry> entries = new ArrayList<>();
|
||||
String query = "SELECT * FROM " + TABLE_NAME + " WHERE collection = ?";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
String key = resultSet.getString("id");
|
||||
String metadata = resultSet.getString("metadata");
|
||||
String embedding = resultSet.getString("embedding");
|
||||
String timestamp = resultSet.getString("timestamp");
|
||||
ZonedDateTime zonedDateTime = parseDatetime(timestamp);
|
||||
entries.add(
|
||||
new DatabaseEntry(
|
||||
key, metadata, embedding, zonedDateTime));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(
|
||||
new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"SELECT * FROM\" failed",
|
||||
e));
|
||||
}
|
||||
return Mono.just(entries);
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<DatabaseEntry> readAsync(String collectionName, String key) {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
String query =
|
||||
"SELECT * FROM "
|
||||
+ TABLE_NAME
|
||||
+ " WHERE collection = ?"
|
||||
+ " AND id = ?";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.setString(2, key != null && !key.isEmpty() ? key : null);
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
String metadata = resultSet.getString("metadata");
|
||||
String embedding = resultSet.getString("embedding");
|
||||
String timestamp = resultSet.getString("timestamp");
|
||||
ZonedDateTime zonedDateTime = parseDatetime(timestamp);
|
||||
return Mono.just(
|
||||
new DatabaseEntry(
|
||||
key, metadata, embedding, zonedDateTime));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(
|
||||
new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"SELECT * FROM\" failed",
|
||||
e));
|
||||
}
|
||||
return Mono.empty();
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String batchQuery(JDBCConnector.BatchOperation operation, Collection<String> keys) {
|
||||
String queryPrefix;
|
||||
switch (operation) {
|
||||
case SELECT:
|
||||
queryPrefix = "SELECT * FROM " + TABLE_NAME + " WHERE collection = ?";
|
||||
break;
|
||||
case DELETE:
|
||||
queryPrefix = "DELETE FROM " + TABLE_NAME + " WHERE collection = ?";
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid batch operation");
|
||||
}
|
||||
StringBuilder queryBuilder = new StringBuilder(queryPrefix);
|
||||
queryBuilder.append(" AND id IN (");
|
||||
|
||||
// Add placeholders for each key
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
queryBuilder.append("?");
|
||||
if (i < keys.size() - 1) {
|
||||
queryBuilder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
queryBuilder.append(")");
|
||||
return queryBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<DatabaseEntry>> readBatchAsync(
|
||||
String collectionName, Collection<String> keys) {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
Collection<DatabaseEntry> entries = new ArrayList<>();
|
||||
String query = batchQuery(JDBCConnector.BatchOperation.SELECT, keys);
|
||||
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
int index = 2;
|
||||
for (String key : keys) {
|
||||
statement.setString(index++, key);
|
||||
}
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
|
||||
while (resultSet.next()) {
|
||||
String key = resultSet.getString("id");
|
||||
String metadata = resultSet.getString("metadata");
|
||||
String embedding = resultSet.getString("embedding");
|
||||
String timestamp = resultSet.getString("timestamp");
|
||||
ZonedDateTime zonedDateTime = parseDatetime(timestamp);
|
||||
entries.add(
|
||||
new DatabaseEntry(
|
||||
key, metadata, embedding, zonedDateTime));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(
|
||||
new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"SELECT * FROM\" failed",
|
||||
e));
|
||||
}
|
||||
return Mono.just(entries);
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteAsync(String collectionName, String key) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"DELETE FROM "
|
||||
+ TABLE_NAME
|
||||
+ " WHERE collection = ?"
|
||||
+ " AND id = ?";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.setString(2, key != null && !key.isEmpty() ? key : null);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"DELETE FROM\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteEmptyAsync(String collectionName) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"DELETE FROM "
|
||||
+ TABLE_NAME
|
||||
+ " WHERE collection = ?"
|
||||
+ " AND id is NULL";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
statement.setString(1, collectionName);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"DELETE FROM\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.mysql;
|
||||
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.JDBCMemoryStore;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLConnector;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLMemoryStore;
|
||||
import java.sql.Connection;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class MySQLMemoryStore extends JDBCMemoryStore {
|
||||
private MySQLMemoryStore(SQLConnector connector) {
|
||||
super(connector);
|
||||
}
|
||||
|
||||
/** Builds a MySQLMemoryStore. */
|
||||
public static class Builder implements SQLMemoryStore.Builder<MySQLMemoryStore> {
|
||||
private Connection connection;
|
||||
|
||||
/**
|
||||
* Builds and returns a MySQLMemoryStore instance with the specified database connection.
|
||||
* The build process will connect to the database and create the required tables.
|
||||
*
|
||||
* @return A MySQLMemoryStore instance configured with the provided database connection.
|
||||
* @deprecated Use {@link #buildAsync()} instead.
|
||||
*/
|
||||
@Override
|
||||
@Deprecated
|
||||
public MySQLMemoryStore build() {
|
||||
return this.buildAsync().block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously builds and returns a MySQLMemoryStore instance with the specified database
|
||||
* connection.
|
||||
*
|
||||
* @return A Mono with a MySQLMemoryStore instance configured with the provided database
|
||||
* connection.
|
||||
*/
|
||||
@Override
|
||||
@CheckReturnValue
|
||||
public Mono<MySQLMemoryStore> buildAsync() {
|
||||
MySQLConnector connector = new MySQLConnector(connection);
|
||||
MySQLMemoryStore memoryStore = new MySQLMemoryStore(connector);
|
||||
return connector.createTableAsync().thenReturn(memoryStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the MySQL database connection to be used by the MySQL memory store being built.
|
||||
*
|
||||
* @param connection The Connection object representing the MySQL database connection.
|
||||
* @return The updated Builder instance to continue the building process for a
|
||||
* MySQLMemoryStore.
|
||||
*/
|
||||
@Override
|
||||
public Builder withConnection(Connection connection) {
|
||||
this.connection = connection;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-connectors-memory-postgresql</artifactId>
|
||||
<name>Semantic Kernel PostgreSQL Memory Connector</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-jdbc</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="azure" name="Azure">
|
||||
<configuration>
|
||||
<option name="dotAzurePath" value="$MODULE_DIR$/.azure" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
</module>
|
||||
-155
@@ -1,155 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.postgresql;
|
||||
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.DatabaseEntry;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.JDBCConnector;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLConnector;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLConnectorException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class PostgreSQLConnector extends JDBCConnector implements SQLConnector {
|
||||
public PostgreSQLConnector(Connection connection) {
|
||||
super(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> createTableAsync() {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String createCollectionKeyTable =
|
||||
"CREATE TABLE IF NOT EXISTS "
|
||||
+ COLLECTIONS_TABLE_NAME
|
||||
+ " ("
|
||||
+ "id TEXT PRIMARY KEY"
|
||||
+ " )";
|
||||
|
||||
String createSKMemoryTable =
|
||||
"CREATE TABLE IF NOT EXISTS "
|
||||
+ TABLE_NAME
|
||||
+ " ("
|
||||
+ "collection TEXT NOT NULL, "
|
||||
+ "key TEXT NOT NULL, "
|
||||
+ "metadata TEXT, "
|
||||
+ "embedding TEXT, "
|
||||
+ "timestamp TEXT, "
|
||||
+ "PRIMARY KEY (collection, key), "
|
||||
+ "FOREIGN KEY (collection) REFERENCES "
|
||||
+ COLLECTIONS_TABLE_NAME
|
||||
+ "(id)"
|
||||
+ " )";
|
||||
|
||||
String createIndex =
|
||||
"CREATE INDEX IF NOT EXISTS "
|
||||
+ INDEX_NAME
|
||||
+ " ON "
|
||||
+ TABLE_NAME
|
||||
+ "(collection)";
|
||||
|
||||
try (Statement statement = this.connection.createStatement()) {
|
||||
statement.addBatch(createCollectionKeyTable);
|
||||
statement.addBatch(createSKMemoryTable);
|
||||
statement.addBatch(createIndex);
|
||||
statement.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"CREATE TABLE\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> upsertAsync(
|
||||
String collection,
|
||||
String key,
|
||||
String metadata,
|
||||
String embedding,
|
||||
ZonedDateTime timestamp) {
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"INSERT INTO "
|
||||
+ TABLE_NAME
|
||||
+ " (collection, key, metadata, embedding, timestamp)"
|
||||
+ " VALUES (?, ?, ?, ?, ?) ON CONFLICT (collection,"
|
||||
+ " key) DO UPDATE SET metadata = EXCLUDED.metadata,"
|
||||
+ " embedding = EXCLUDED.embedding, timestamp ="
|
||||
+ " EXCLUDED.timestamp";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
String metadataString = metadata != null ? metadata : "";
|
||||
String embeddingString = embedding != null ? embedding : "";
|
||||
String timestampString = formatDatetime(timestamp);
|
||||
statement.setString(1, collection);
|
||||
statement.setString(2, key);
|
||||
statement.setString(3, metadataString);
|
||||
statement.setString(4, embeddingString);
|
||||
statement.setString(5, timestampString);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"INSERT INTO\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.thenReturn(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Collection<String>> upsertBatchAsync(
|
||||
String collection, Collection<DatabaseEntry> records) {
|
||||
Collection<String> keys = new ArrayList<>();
|
||||
return Mono.fromRunnable(
|
||||
() -> {
|
||||
String query =
|
||||
"INSERT INTO "
|
||||
+ TABLE_NAME
|
||||
+ " (collection, key, metadata, embedding, timestamp)"
|
||||
+ " VALUES (?, ?, ?, ?, ?) ON CONFLICT (collection,"
|
||||
+ " key) DO UPDATE SET metadata = EXCLUDED.metadata,"
|
||||
+ " embedding = EXCLUDED.embedding, timestamp ="
|
||||
+ " EXCLUDED.timestamp";
|
||||
try (PreparedStatement statement =
|
||||
this.connection.prepareStatement(query)) {
|
||||
for (DatabaseEntry entry : records) {
|
||||
String metadataString =
|
||||
entry.getMetadata() != null ? entry.getMetadata() : "";
|
||||
String embeddingString =
|
||||
entry.getEmbedding() != null
|
||||
? entry.getEmbedding()
|
||||
: "";
|
||||
String timestampString = formatDatetime(entry.getTimestamp());
|
||||
statement.setString(1, collection);
|
||||
statement.setString(2, entry.getKey());
|
||||
statement.setString(3, metadataString);
|
||||
statement.setString(4, embeddingString);
|
||||
statement.setString(5, timestampString);
|
||||
statement.addBatch();
|
||||
keys.add(entry.getKey());
|
||||
}
|
||||
|
||||
statement.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLConnectorException(
|
||||
SQLConnectorException.ErrorCodes.SQL_ERROR,
|
||||
"\"INSERT INTO\" failed",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.thenReturn(keys);
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.postgresql;
|
||||
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.JDBCMemoryStore;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLConnector;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLMemoryStore;
|
||||
import java.sql.Connection;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class PostgreSQLMemoryStore extends JDBCMemoryStore {
|
||||
private PostgreSQLMemoryStore(SQLConnector connector) {
|
||||
super(connector);
|
||||
}
|
||||
|
||||
/** Builds a PostgreSQLMemoryStore. */
|
||||
public static class Builder implements SQLMemoryStore.Builder<PostgreSQLMemoryStore> {
|
||||
private Connection connection;
|
||||
|
||||
/**
|
||||
* Builds and returns a PostgreSQLMemoryStore instance with the specified database
|
||||
* connection. The build process will connect to the database and create the required
|
||||
* tables.
|
||||
*
|
||||
* @return A PostgreSQLMemoryStore instance configured with the provided database
|
||||
* connection.
|
||||
* @deprecated Use {@link #buildAsync()} instead.
|
||||
*/
|
||||
@Override
|
||||
@Deprecated
|
||||
public PostgreSQLMemoryStore build() {
|
||||
return this.buildAsync().block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously builds and returns a PostgreSQLMemoryStore instance with the specified
|
||||
* database connection.
|
||||
*
|
||||
* @return A Mono with a PostgreSQLMemoryStore instance configured with the provided
|
||||
* database connection.
|
||||
*/
|
||||
@Override
|
||||
@CheckReturnValue
|
||||
public Mono<PostgreSQLMemoryStore> buildAsync() {
|
||||
PostgreSQLConnector connector = new PostgreSQLConnector(connection);
|
||||
PostgreSQLMemoryStore memoryStore = new PostgreSQLMemoryStore(connector);
|
||||
return connector.createTableAsync().thenReturn(memoryStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PostgreSQL database connection to be used by the PostgreSQL memory store being
|
||||
* built.
|
||||
*
|
||||
* @param connection The Connection object representing the PostgreSQL database connection.
|
||||
* @return The updated Builder instance to continue the building process for a
|
||||
* PostgreSQLMemoryStore.
|
||||
*/
|
||||
@Override
|
||||
public Builder withConnection(Connection connection) {
|
||||
this.connection = connection;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-connectors-memory-sqlite</artifactId>
|
||||
|
||||
<name>Semantic Kernel SQLite Memory Connector</name>
|
||||
<description>Defines connector for SQLite to be used with the Semantic Kernel</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-connectors-memory-jdbc</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.sqlite;
|
||||
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.JDBCConnector;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.JDBCMemoryStore;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLConnector;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.SQLMemoryStore;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class SQLiteMemoryStore extends JDBCMemoryStore {
|
||||
private SQLiteMemoryStore(SQLConnector connector) {
|
||||
super(connector);
|
||||
}
|
||||
|
||||
/** Builds a SQLiteMemoryStore. */
|
||||
public static class Builder implements SQLMemoryStore.Builder<SQLiteMemoryStore> {
|
||||
private Connection connection;
|
||||
|
||||
/**
|
||||
* Builds and returns an SQLiteMemoryStore instance with the specified database connection.
|
||||
* The build process will connect to the database and create the required tables.
|
||||
*
|
||||
* @return An SQLiteMemoryStore instance configured with the provided database connection.
|
||||
* @deprecated Use {@link #buildAsync()} instead.
|
||||
*/
|
||||
@Override
|
||||
@Deprecated
|
||||
public SQLiteMemoryStore build() {
|
||||
return this.buildAsync().block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously builds and returns an SQLiteMemoryStore instance with the specified
|
||||
* database connection.
|
||||
*
|
||||
* @return A Mono with an SQLiteMemoryStore instance configured with the provided SQLite
|
||||
* database connection.
|
||||
*/
|
||||
@Override
|
||||
@CheckReturnValue
|
||||
public Mono<SQLiteMemoryStore> buildAsync() {
|
||||
JDBCConnector connector = new JDBCConnector(connection);
|
||||
SQLiteMemoryStore memoryStore = new SQLiteMemoryStore(connector);
|
||||
return connector.createTableAsync().thenReturn(memoryStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SQLite database connection to be used by the SQLite memory store being built.
|
||||
*
|
||||
* @param connection The Connection object representing the SQLite database connection.
|
||||
* @return The updated Builder instance to continue the building process for an
|
||||
* SQLiteMemoryStore.
|
||||
*/
|
||||
@Override
|
||||
public Builder withConnection(Connection connection) {
|
||||
this.connection = connection;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and sets an SQLite database connection using the specified filename. This will
|
||||
* overwrite any previously set connection.
|
||||
*
|
||||
* @param filename The filename of the SQLite database.
|
||||
* @return The updated Builder instance with the SQLite database connection set.
|
||||
* @throws SQLException If there is an issue with establishing the database connection.
|
||||
*/
|
||||
public Builder withFilename(String filename) throws SQLException {
|
||||
return withConnection(DriverManager.getConnection("jdbc:sqlite:" + filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-10
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -6,13 +6,14 @@
|
||||
<parent>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.14-alpha-SNAPSHOT</version>
|
||||
<version>1.0.0-rc2-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-planners</artifactId>
|
||||
<name>Semantic Kernel Planners Extension</name>
|
||||
<description>Planner implementation for Semantic Kernel</description>
|
||||
<artifactId>semantickernel-connectors-web-bing</artifactId>
|
||||
|
||||
<name>Semantic Kernel Bing Connector</name>
|
||||
<description>Defines Bing connectors to be used with the Semantic Kernel</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -29,11 +30,6 @@
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,421 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<profiles version="13">
|
||||
<profile kind="CodeFormatterProfile" name="Eclipse" version="13">
|
||||
<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="10"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.compliance" value="10"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.source" value="10"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines" value="2147483647"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression"
|
||||
value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_module_statements" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_arguments" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_parameters" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration"
|
||||
value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="100"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments"
|
||||
value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header"
|
||||
value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header"
|
||||
value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header"
|
||||
value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
|
||||
<setting
|
||||
id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
|
||||
<setting
|
||||
id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
|
||||
<setting
|
||||
id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference"
|
||||
value="do not insert"/>
|
||||
<setting
|
||||
id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw"
|
||||
value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression"
|
||||
value="do not insert"/>
|
||||
<setting
|
||||
id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation"
|
||||
value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="100"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration"
|
||||
value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause" value="common_lines"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_assignment_operator" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_conditional_operator" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
|
||||
</profile>
|
||||
</profiles>
|
||||
-306
@@ -1,306 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel;
|
||||
|
||||
import com.microsoft.semantickernel.settings.GPT3Settings;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
public class GPT3Tokenizer {
|
||||
/**
|
||||
* Port of GPT3 Javascript tokenizer recommended by OpenAI. See <a
|
||||
* href="https://platform.openai.com/tokenizer">GPT3 tokernizer</a> and <a
|
||||
* href="https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them">What
|
||||
* are tokens and how to count them</a>
|
||||
*/
|
||||
private static final char[] bytesToUnicode =
|
||||
new char[] {
|
||||
(char) 0x0100, (char) 0x0101, (char) 0x0102, (char) 0x0103, (char) 0x0104,
|
||||
(char) 0x0105, (char) 0x0106, (char) 0x0107,
|
||||
(char) 0x0108, (char) 0x0109, (char) 0x010A, (char) 0x010B, (char) 0x010C,
|
||||
(char) 0x010D, (char) 0x010E, (char) 0x010F,
|
||||
(char) 0x0110, (char) 0x0111, (char) 0x0112, (char) 0x0113, (char) 0x0114,
|
||||
(char) 0x0115, (char) 0x0116, (char) 0x0117,
|
||||
(char) 0x0118, (char) 0x0119, (char) 0x011A, (char) 0x011B, (char) 0x011C,
|
||||
(char) 0x011D, (char) 0x011E, (char) 0x011F,
|
||||
(char) 0x0120, (char) 0x0021, (char) 0x0022, (char) 0x0023, (char) 0x0024,
|
||||
(char) 0x0025, (char) 0x0026, (char) 0x0027,
|
||||
(char) 0x0028, (char) 0x0029, (char) 0x002A, (char) 0x002B, (char) 0x002C,
|
||||
(char) 0x002D, (char) 0x002E, (char) 0x002F,
|
||||
(char) 0x0030, (char) 0x0031, (char) 0x0032, (char) 0x0033, (char) 0x0034,
|
||||
(char) 0x0035, (char) 0x0036, (char) 0x0037,
|
||||
(char) 0x0038, (char) 0x0039, (char) 0x003A, (char) 0x003B, (char) 0x003C,
|
||||
(char) 0x003D, (char) 0x003E, (char) 0x003F,
|
||||
(char) 0x0040, (char) 0x0041, (char) 0x0042, (char) 0x0043, (char) 0x0044,
|
||||
(char) 0x0045, (char) 0x0046, (char) 0x0047,
|
||||
(char) 0x0048, (char) 0x0049, (char) 0x004A, (char) 0x004B, (char) 0x004C,
|
||||
(char) 0x004D, (char) 0x004E, (char) 0x004F,
|
||||
(char) 0x0050, (char) 0x0051, (char) 0x0052, (char) 0x0053, (char) 0x0054,
|
||||
(char) 0x0055, (char) 0x0056, (char) 0x0057,
|
||||
(char) 0x0058, (char) 0x0059, (char) 0x005A, (char) 0x005B, (char) 0x005C,
|
||||
(char) 0x005D, (char) 0x005E, (char) 0x005F,
|
||||
(char) 0x0060, (char) 0x0061, (char) 0x0062, (char) 0x0063, (char) 0x0064,
|
||||
(char) 0x0065, (char) 0x0066, (char) 0x0067,
|
||||
(char) 0x0068, (char) 0x0069, (char) 0x006A, (char) 0x006B, (char) 0x006C,
|
||||
(char) 0x006D, (char) 0x006E, (char) 0x006F,
|
||||
(char) 0x0070, (char) 0x0071, (char) 0x0072, (char) 0x0073, (char) 0x0074,
|
||||
(char) 0x0075, (char) 0x0076, (char) 0x0077,
|
||||
(char) 0x0078, (char) 0x0079, (char) 0x007A, (char) 0x007B, (char) 0x007C,
|
||||
(char) 0x007D, (char) 0x007E, (char) 0x0121,
|
||||
(char) 0x0122, (char) 0x0123, (char) 0x0124, (char) 0x0125, (char) 0x0126,
|
||||
(char) 0x0127, (char) 0x0128, (char) 0x0129,
|
||||
(char) 0x012A, (char) 0x012B, (char) 0x012C, (char) 0x012D, (char) 0x012E,
|
||||
(char) 0x012F, (char) 0x0130, (char) 0x0131,
|
||||
(char) 0x0132, (char) 0x0133, (char) 0x0134, (char) 0x0135, (char) 0x0136,
|
||||
(char) 0x0137, (char) 0x0138, (char) 0x0139,
|
||||
(char) 0x013A, (char) 0x013B, (char) 0x013C, (char) 0x013D, (char) 0x013E,
|
||||
(char) 0x013F, (char) 0x0140, (char) 0x0141,
|
||||
(char) 0x0142, (char) 0x00A1, (char) 0x00A2, (char) 0x00A3, (char) 0x00A4,
|
||||
(char) 0x00A5, (char) 0x00A6, (char) 0x00A7,
|
||||
(char) 0x00A8, (char) 0x00A9, (char) 0x00AA, (char) 0x00AB, (char) 0x00AC,
|
||||
(char) 0x0143, (char) 0x00AE, (char) 0x00AF,
|
||||
(char) 0x00B0, (char) 0x00B1, (char) 0x00B2, (char) 0x00B3, (char) 0x00B4,
|
||||
(char) 0x00B5, (char) 0x00B6, (char) 0x00B7,
|
||||
(char) 0x00B8, (char) 0x00B9, (char) 0x00BA, (char) 0x00BB, (char) 0x00BC,
|
||||
(char) 0x00BD, (char) 0x00BE, (char) 0x00BF,
|
||||
(char) 0x00C0, (char) 0x00C1, (char) 0x00C2, (char) 0x00C3, (char) 0x00C4,
|
||||
(char) 0x00C5, (char) 0x00C6, (char) 0x00C7,
|
||||
(char) 0x00C8, (char) 0x00C9, (char) 0x00CA, (char) 0x00CB, (char) 0x00CC,
|
||||
(char) 0x00CD, (char) 0x00CE, (char) 0x00CF,
|
||||
(char) 0x00D0, (char) 0x00D1, (char) 0x00D2, (char) 0x00D3, (char) 0x00D4,
|
||||
(char) 0x00D5, (char) 0x00D6, (char) 0x00D7,
|
||||
(char) 0x00D8, (char) 0x00D9, (char) 0x00DA, (char) 0x00DB, (char) 0x00DC,
|
||||
(char) 0x00DD, (char) 0x00DE, (char) 0x00DF,
|
||||
(char) 0x00E0, (char) 0x00E1, (char) 0x00E2, (char) 0x00E3, (char) 0x00E4,
|
||||
(char) 0x00E5, (char) 0x00E6, (char) 0x00E7,
|
||||
(char) 0x00E8, (char) 0x00E9, (char) 0x00EA, (char) 0x00EB, (char) 0x00EC,
|
||||
(char) 0x00ED, (char) 0x00EE, (char) 0x00EF,
|
||||
(char) 0x00F0, (char) 0x00F1, (char) 0x00F2, (char) 0x00F3, (char) 0x00F4,
|
||||
(char) 0x00F5, (char) 0x00F6, (char) 0x00F7,
|
||||
(char) 0x00F8, (char) 0x00F9, (char) 0x00FA, (char) 0x00FB, (char) 0x00FC,
|
||||
(char) 0x00FD, (char) 0x00FE, (char) 0x00FF
|
||||
};
|
||||
|
||||
// Regex for English contractions, e.g. "he's", "we'll", "I'm" etc.
|
||||
private static final Pattern encodingRegex =
|
||||
Pattern.compile(
|
||||
"'s|'t|'re|'ve|'m|'ll|'d| ?\\p{L}+| ?\\p{N}+|"
|
||||
+ " ?[^\\s\\p{L}\\p{N}]+|\\s+(?!\\S)|\\s+");
|
||||
|
||||
private static final Map<String, List<String>> bpeCache;
|
||||
private static final String MAX_TOKENIZER_CACHE_SIZE_KEY = "MAX_TOKENIZER_CACHE_SIZE";
|
||||
private static final String MAX_TOKENIZER_CACHE_SIZE_DEFAULT = "100000";
|
||||
|
||||
static {
|
||||
// Limit the maximum cache size
|
||||
int cacheSize =
|
||||
Integer.parseInt(
|
||||
System.getProperty(
|
||||
MAX_TOKENIZER_CACHE_SIZE_KEY, MAX_TOKENIZER_CACHE_SIZE_DEFAULT));
|
||||
bpeCache =
|
||||
new LinkedHashMap<String, List<String>>() {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, List<String>> eldest) {
|
||||
return size() > cacheSize;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The tokenizer uses a byte-pair encoding (BPE) algorithm to split words into sub-words based
|
||||
* on frequency and merges rules. It can handle out-of-vocabulary words, punctuation, and
|
||||
* special tokens.
|
||||
*
|
||||
* @param text Text to tokenize
|
||||
* @return List of token IDs
|
||||
*/
|
||||
public static List<Integer> encode(String text) {
|
||||
List<Integer> bpeTokens = new ArrayList<>();
|
||||
|
||||
if (text != null && !text.isEmpty()) {
|
||||
// Find all the matches.
|
||||
Matcher matcher = encodingRegex.matcher(text);
|
||||
|
||||
// Determine the maximum number of UTF8 bytes that any match value could require.
|
||||
int maxUtf8Length = 0;
|
||||
while (matcher.find()) {
|
||||
String matchValue = matcher.group();
|
||||
int utf8Length = encodingUtf8GetByteCount(matchValue);
|
||||
if (utf8Length > maxUtf8Length) {
|
||||
maxUtf8Length = utf8Length;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have a sufficient char array to accommodate maxUtf8Length chars.
|
||||
// The byte-to-char mapping scheme employed is 1:1, so we'll end up needing 1 char
|
||||
// for every 1 UTF8 byte.
|
||||
char[] chars = new char[maxUtf8Length];
|
||||
|
||||
// Rather than using separate space for the UTF8 bytes, we just reinterpret the char
|
||||
// array as a byte array. Since our mapping is 1:1,
|
||||
// the space required for the bytes will always be half of the space required for
|
||||
// the chars. We can UTF8-encode into the first half,
|
||||
// and then walk backwards through the bytes, using the byte-to-char mapping
|
||||
// scheme to populate the chars from the back to the front. By going in reverse,
|
||||
// we guarantee we won't overwrite any bytes we haven't yet seen.
|
||||
byte[] bytes = new byte[maxUtf8Length];
|
||||
|
||||
// Now that our space is created, do the actual encoding.
|
||||
matcher.reset();
|
||||
while (matcher.find()) {
|
||||
String matchValue = matcher.group();
|
||||
|
||||
// Encode the UTF8 bytes.
|
||||
int bytesWritten = encodingUtf8GetBytes(matchValue, bytes);
|
||||
|
||||
// Translate those bytes into chars.
|
||||
for (int i = bytesWritten - 1; i >= 0; i--) {
|
||||
chars[i] = bytesToUnicode[bytes[i] & 0xFF];
|
||||
}
|
||||
|
||||
// Evaluate the BPE for the encoded chars.
|
||||
for (String encoding : bytePairEncoding(new String(chars, 0, bytesWritten))) {
|
||||
bpeTokens.add(GPT3Settings.encoder.get(encoding));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bpeTokens;
|
||||
}
|
||||
|
||||
public static List<Integer> encode(StringBuilder stringBuilder) {
|
||||
if (stringBuilder != null) {
|
||||
return encode(stringBuilder.toString());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Integer> encode(char[] chars) {
|
||||
if (chars != null) {
|
||||
return encode(new String(chars));
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Integer> encode(Iterable<Character> chars) {
|
||||
if (chars != null) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Character c : chars) {
|
||||
sb.append(c);
|
||||
}
|
||||
return encode(sb.toString());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static int encodingUtf8GetByteCount(String str) {
|
||||
return str.getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
private static int encodingUtf8GetBytes(String str, byte[] bytes) {
|
||||
assert str != null;
|
||||
assert bytes != null;
|
||||
|
||||
byte[] strBytes = str.getBytes(StandardCharsets.UTF_8);
|
||||
System.arraycopy(strBytes, 0, bytes, 0, strBytes.length);
|
||||
return strBytes.length;
|
||||
}
|
||||
|
||||
private static List<String> bytePairEncoding(String token) {
|
||||
if (bpeCache.containsKey(token)) {
|
||||
return bpeCache.get(token);
|
||||
}
|
||||
|
||||
if (token.length() <= 1) {
|
||||
List<String> list = new ArrayList<>(1);
|
||||
list.add(token);
|
||||
bpeCache.put(token, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
List<String> word = new ArrayList<>(token.length());
|
||||
for (char c : token.toCharArray()) {
|
||||
word.add(String.valueOf(c));
|
||||
}
|
||||
|
||||
long smallestRank = Long.MAX_VALUE;
|
||||
Tuple2<String, String> smallestPair = Tuples.of("", "");
|
||||
List<String> newWord;
|
||||
|
||||
while (word.size() >= 2) {
|
||||
for (int pairIndex = 0; pairIndex < word.size() - 1; pairIndex++) {
|
||||
Tuple2<String, String> pair =
|
||||
Tuples.of(word.get(pairIndex), word.get(pairIndex + 1));
|
||||
|
||||
long pairRank =
|
||||
GPT3Settings.bpeRanks.containsKey(pair)
|
||||
? GPT3Settings.bpeRanks.get(pair)
|
||||
: 100_000_000_000L;
|
||||
|
||||
if (pairRank <= smallestRank) {
|
||||
smallestRank = pairRank;
|
||||
smallestPair = pair;
|
||||
}
|
||||
}
|
||||
|
||||
if (!GPT3Settings.bpeRanks.containsKey(smallestPair)) {
|
||||
break;
|
||||
}
|
||||
|
||||
String first = smallestPair.getT1();
|
||||
String second = smallestPair.getT2();
|
||||
|
||||
newWord = new ArrayList<>(word.size());
|
||||
for (int i = 0; i < word.size(); i++) {
|
||||
int j = word.subList(i, word.size()).indexOf(first);
|
||||
|
||||
// Adjust j as sublist was used
|
||||
if (j >= 0) {
|
||||
j += i;
|
||||
}
|
||||
|
||||
int limit = j < 0 ? word.size() : j;
|
||||
for (int copy = i; copy < limit; copy++) {
|
||||
newWord.add(word.get(copy));
|
||||
}
|
||||
|
||||
if (j < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
i = j;
|
||||
|
||||
if (i < (word.size() - 1)
|
||||
&& word.get(i).equals(first)
|
||||
&& word.get(i + 1).equals(second)) {
|
||||
newWord.add(first + second);
|
||||
i++;
|
||||
} else {
|
||||
newWord.add(word.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Swap the new word in for the old
|
||||
List<String> temp = word;
|
||||
word = newWord;
|
||||
newWord = temp;
|
||||
|
||||
// And reset state for the next go-around
|
||||
newWord.clear();
|
||||
smallestRank = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
bpeCache.put(token, word);
|
||||
return word;
|
||||
}
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.settings;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class EmbeddedResource {
|
||||
/**
|
||||
* Return content of BPE file.
|
||||
*
|
||||
* @return BPE file content
|
||||
*/
|
||||
public static String readBytePairEncodingTable() throws IOException {
|
||||
return readFile("vocab.bpe");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return content of encoding table file.
|
||||
*
|
||||
* @return Encoding table string
|
||||
*/
|
||||
public static String readEncodingTable() throws IOException {
|
||||
return readFile("encoder.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a content file embedded in the project. Files are read from disk, not from the classpath
|
||||
* or the JAR file, to avoid inflating the JAR size.
|
||||
*
|
||||
* @param fileName Filename to read
|
||||
* @return File content
|
||||
* @throws FileNotFoundException Error in case the file doesn't exist
|
||||
*/
|
||||
private static String readFile(String fileName) throws IOException {
|
||||
ClassLoader classLoader = EmbeddedResource.class.getClassLoader();
|
||||
InputStream inputStream = classLoader.getResourceAsStream(fileName);
|
||||
|
||||
if (inputStream == null) {
|
||||
throw new FileNotFoundException("File not found: " + fileName);
|
||||
}
|
||||
|
||||
try (InputStreamReader streamReader =
|
||||
new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
BufferedReader reader = new BufferedReader(streamReader)) {
|
||||
|
||||
String line;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while ((line = reader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
builder.append(System.lineSeparator());
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.settings;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.semantickernel.ai.AIException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
public class GPT3Settings {
|
||||
|
||||
public static final Map<String, Integer> encoder;
|
||||
public static final Map<Tuple2<String, String>, Integer> bpeRanks;
|
||||
|
||||
static {
|
||||
try {
|
||||
encoder = getEncoder();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
bpeRanks = getBpeRanks();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the cached encoding table (encoder.json).
|
||||
private static Map<String, Integer> getEncoder() throws IOException {
|
||||
try {
|
||||
String encodingTableJson = EmbeddedResource.readEncodingTable();
|
||||
|
||||
// Read the JSON file and convert it to a Map
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
return objectMapper.readValue(
|
||||
encodingTableJson, new TypeReference<Map<String, Integer>>() {});
|
||||
} catch (IOException e) {
|
||||
System.out.println(e.getMessage());
|
||||
|
||||
throw new AIException(
|
||||
AIException.ErrorCodes.INVALID_CONFIGURATION,
|
||||
"Encoding table deserialization returned NULL");
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the cached byte pair encoding table (vocab.bpe).
|
||||
|
||||
private static Map<Tuple2<String, String>, Integer> getBpeRanks() throws IOException {
|
||||
String table = EmbeddedResource.readBytePairEncodingTable();
|
||||
|
||||
// Skip past the header line
|
||||
int pos = table.indexOf('\n') + 1;
|
||||
|
||||
// For each line, split on the first space and add the pair to the map as a key with the
|
||||
// value being the entry number.
|
||||
Map<Tuple2<String, String>, Integer> result = new HashMap<>();
|
||||
int nextPos;
|
||||
while ((nextPos = table.indexOf('\n', pos)) >= 0) {
|
||||
String line = table.substring(pos, nextPos).trim();
|
||||
pos = line.indexOf(' ');
|
||||
if (pos >= 0) {
|
||||
String first = line.substring(0, pos);
|
||||
String second = line.substring(pos + 1);
|
||||
result.put(Tuples.of(first, second), result.size());
|
||||
}
|
||||
pos = nextPos + 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
-31
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class GPT3TokenizerTest {
|
||||
|
||||
@Test
|
||||
public void tokenizerTest() {
|
||||
String sentence = "Some text on one line";
|
||||
int tokenCount = GPT3Tokenizer.encode(sentence).size();
|
||||
Assertions.assertEquals(5, tokenCount);
|
||||
|
||||
sentence = "Some text on\r\ntwo lines";
|
||||
tokenCount = GPT3Tokenizer.encode(sentence).size();
|
||||
Assertions.assertEquals(7, tokenCount);
|
||||
|
||||
// Sample from: https://platform.openai.com/tokenizer
|
||||
sentence =
|
||||
"Many words map to one token, but some don't: indivisible.\n"
|
||||
+ "\n"
|
||||
+ "Unicode characters like emojis may be split into many tokens containing the"
|
||||
+ " underlying bytes: \uD83E\uDD1A\uD83C\uDFFE\n"
|
||||
+ "\n"
|
||||
+ "Sequences of characters commonly found next to each other may be grouped"
|
||||
+ " together: 1234567890";
|
||||
tokenCount = GPT3Tokenizer.encode(sentence).size();
|
||||
Assertions.assertEquals(64, tokenCount);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.actionplanner;
|
||||
|
||||
/// <summary>
|
||||
/// Plan data structure returned by the basic planner semantic function
|
||||
/// </summary>
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Map;
|
||||
|
||||
public class ActionPlanResponse {
|
||||
|
||||
public static class PlanData {
|
||||
/// <summary>
|
||||
/// Rationale given by the LLM for choosing the function
|
||||
/// </summary>
|
||||
public final String rationale;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the function chosen
|
||||
/// </summary>
|
||||
public final String function;
|
||||
|
||||
/// <summary>
|
||||
/// Parameter values
|
||||
/// </summary>
|
||||
public final Map<String, String> parameters;
|
||||
|
||||
@JsonCreator
|
||||
public PlanData(
|
||||
@JsonProperty("plan") String rationale,
|
||||
@JsonProperty("function") String function,
|
||||
@JsonProperty("parameters") Map<String, String> parameters) {
|
||||
this.rationale = rationale;
|
||||
this.function = function;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
}
|
||||
|
||||
public final PlanData plan;
|
||||
|
||||
@JsonCreator
|
||||
public ActionPlanResponse(@JsonProperty("plan") PlanData plan) {
|
||||
this.plan = plan;
|
||||
}
|
||||
}
|
||||
-364
@@ -1,364 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.actionplanner;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.orchestration.SKFunction;
|
||||
import com.microsoft.semantickernel.orchestration.WritableContextVariables;
|
||||
import com.microsoft.semantickernel.planner.PlanningException;
|
||||
import com.microsoft.semantickernel.skilldefinition.ReadOnlySkillCollection;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionRequestSettings;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/// <summary>
|
||||
/// Action Planner allows to select one function out of many, to achieve a given goal.
|
||||
/// The planner implement the Intent Detection pattern, uses the functions registered
|
||||
/// in the kernel to see if there's a relevant one, providing instructions to call the
|
||||
/// function and the rationale used to select it. The planner can also return
|
||||
/// "no function" is nothing relevant is available.
|
||||
/// The rationale is currently available only in the prompt, we might include it in
|
||||
/// the Plan object in future.
|
||||
/// </summary>
|
||||
public class ActionPlanner {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ActionPlanner.class);
|
||||
|
||||
// Extracts the json portion of a plan between the braces, stripping off any non-json content
|
||||
private static final Pattern CLEAN_PLAN =
|
||||
Pattern.compile("[^{]*(\\{.*})[^}]*", Pattern.MULTILINE | Pattern.DOTALL);
|
||||
private static final String StopSequence = "#END-OF-PLAN";
|
||||
private static final String SkillName = "this";
|
||||
|
||||
// Planner semantic function
|
||||
private final CompletionSKFunction plannerFunction;
|
||||
|
||||
// Context used to access the list of functions in the kernel
|
||||
private SKContext context;
|
||||
private Kernel kernel;
|
||||
|
||||
// TODO: allow to inject skill store
|
||||
|
||||
/**
|
||||
* Initialize a new instance of the ActionPlanner class
|
||||
*
|
||||
* @param kernel The semantic kernel instance
|
||||
* @param prompt prompt override
|
||||
*/
|
||||
public ActionPlanner(Kernel kernel, @Nullable String prompt) {
|
||||
// Verify.NotNull(kernel);
|
||||
String promptTemplate;
|
||||
if (prompt == null) {
|
||||
promptTemplate = read("skprompt.txt");
|
||||
} else {
|
||||
promptTemplate = prompt;
|
||||
}
|
||||
|
||||
this.plannerFunction =
|
||||
SKBuilders.completionFunctions()
|
||||
.withKernel(kernel)
|
||||
.withPromptTemplate(promptTemplate)
|
||||
.withSkillName(SkillName)
|
||||
.withRequestSettings(
|
||||
SKBuilders.completionRequestSettings()
|
||||
.temperature(0.0)
|
||||
.topP(0.0)
|
||||
.maxTokens(1024)
|
||||
.presencePenalty(0)
|
||||
.frequencyPenalty(0)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
kernel.importSkill(this, SkillName);
|
||||
|
||||
this.kernel = kernel;
|
||||
this.context = SKBuilders.context().withSkills(kernel.getSkills()).build();
|
||||
}
|
||||
|
||||
public static String read(String file) {
|
||||
try (InputStream stream = ActionPlanner.class.getResourceAsStream(file)) {
|
||||
byte[] buffer = new byte[stream.available()];
|
||||
stream.read(buffer);
|
||||
return new String(buffer, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<Plan> createPlanAsync(String goal) {
|
||||
if (goal == null || goal.isEmpty()) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_GOAL, "The goal specified is empty");
|
||||
}
|
||||
|
||||
context = this.context.update(goal);
|
||||
|
||||
return this.plannerFunction
|
||||
.invokeAsync(this.context, new CompletionRequestSettings(0, 0, 0, 0, 2048))
|
||||
.handle(
|
||||
(result, sink) -> {
|
||||
try {
|
||||
sink.next(parsePlan(goal, result));
|
||||
} catch (Exception e) {
|
||||
sink.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Plan parsePlan(String goal, SKContext result) {
|
||||
ActionPlanResponse planData;
|
||||
// Clean up the plan, removing any prompt and the stop sequence
|
||||
Matcher matcher = CLEAN_PLAN.matcher(result.getResult());
|
||||
|
||||
String plan = result.getResult();
|
||||
if (matcher.matches()) {
|
||||
plan = matcher.group(1);
|
||||
}
|
||||
|
||||
try {
|
||||
planData = new ObjectMapper().readValue(plan, ActionPlanResponse.class);
|
||||
} catch (Exception e) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_PLAN,
|
||||
"Plan parsing error, invalid JSON",
|
||||
e);
|
||||
}
|
||||
|
||||
if (planData == null) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_PLAN,
|
||||
"The plan deserialized to a null object");
|
||||
}
|
||||
|
||||
SKFunction function = getPlanFunction(planData);
|
||||
|
||||
WritableContextVariables variables = SKBuilders.variables().build().writableClone();
|
||||
|
||||
planData.plan
|
||||
.parameters
|
||||
.entrySet()
|
||||
.forEach(
|
||||
entry -> {
|
||||
variables.setVariable(entry.getKey(), entry.getValue());
|
||||
});
|
||||
|
||||
if (function == null) {
|
||||
// No function was found - return a plan with no steps.
|
||||
return new Plan(goal, variables, kernel::getSkills, kernel::getService);
|
||||
} else {
|
||||
return new Plan(goal, variables, kernel::getSkills, kernel::getService, function);
|
||||
}
|
||||
}
|
||||
|
||||
public SKFunction getPlanFunction(ActionPlanResponse planData) {
|
||||
|
||||
if (planData.plan.function.contains(".")) {
|
||||
String[] parts = planData.plan.function.split("\\.", -1);
|
||||
SKFunction function =
|
||||
context.getSkills().getFunction(parts[0], parts[1], SKFunction.class);
|
||||
|
||||
if (function == null) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_PLAN,
|
||||
"Unknown function " + planData.plan.function);
|
||||
}
|
||||
return function;
|
||||
|
||||
} else if (!planData.plan.function.isEmpty()) {
|
||||
CompletionSKFunction function =
|
||||
context.getSkills()
|
||||
.getFunction(planData.plan.function, CompletionSKFunction.class);
|
||||
|
||||
if (function == null) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_PLAN,
|
||||
"Unknown skill " + planData.plan.function);
|
||||
}
|
||||
|
||||
return function;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: use goal to find relevant functions in a skill store
|
||||
/// <summary>
|
||||
/// Native function returning a list of all the functions in the current context,
|
||||
/// excluding functions in the planner itself.
|
||||
/// </summary>
|
||||
/// <param name="goal">Currently unused. Will be used to handle long lists of functions.</param>
|
||||
/// <param name="context">Function execution context</param>
|
||||
/// <returns>List of functions, formatted accordingly to the prompt</returns>
|
||||
|
||||
@DefineSKFunction(
|
||||
name = "listOfFunctions",
|
||||
description = "List all functions available in the kernel")
|
||||
@Nullable
|
||||
public String listOfFunctions(
|
||||
@SKFunctionParameters(
|
||||
name = "goal",
|
||||
description = "The current goal processed by the planner",
|
||||
defaultValue = "")
|
||||
String goal,
|
||||
SKContext context) {
|
||||
// Verify.NotNull(context.Skills);
|
||||
ReadOnlySkillCollection skills = context.getSkills();
|
||||
|
||||
return this.populateList(skills);
|
||||
}
|
||||
|
||||
// TODO: generate string programmatically
|
||||
// TODO: use goal to find relevant examples
|
||||
@DefineSKFunction(
|
||||
name = "GoodExamples",
|
||||
description = "List a few good examples of plans to generate")
|
||||
public String goodExamples(
|
||||
@SKFunctionParameters(
|
||||
name = "goal",
|
||||
description = "The current goal processed by the planner",
|
||||
defaultValue = "")
|
||||
String goal,
|
||||
SKContext context) {
|
||||
|
||||
return "\n"
|
||||
+ "[EXAMPLE]\n"
|
||||
+ "- List of functions:\n"
|
||||
+ "// Read a file.\n"
|
||||
+ "FileIOSkill.ReadAsync\n"
|
||||
+ "Parameter \"path\": Source file.\n"
|
||||
+ "// Write a file.\n"
|
||||
+ "FileIOSkill.WriteAsync\n"
|
||||
+ "Parameter \"path\": Destination file. (default value: sample.txt)\n"
|
||||
+ "Parameter \"content\": File content.\n"
|
||||
+ "// Get the current time.\n"
|
||||
+ "TimeSkill.Time\n"
|
||||
+ "No parameters.\n"
|
||||
+ "// Makes a POST request to a uri.\n"
|
||||
+ "HttpSkill.PostAsync\n"
|
||||
+ "Parameter \"body\": The body of the request.\n"
|
||||
+ "- End list of functions.\n"
|
||||
+ "Goal: create a file called \"something.txt\".\n"
|
||||
+ "{\"plan\":{\n"
|
||||
+ "\"rationale\": \"the list contains a function that allows to create files\",\n"
|
||||
+ "\"function\": \"FileIOSkill.WriteAsync\",\n"
|
||||
+ "\"parameters\": {\n"
|
||||
+ "\"path\": \"something.txt\",\n"
|
||||
+ "\"content\": null\n"
|
||||
+ "}}}\n"
|
||||
+ "#END-OF-PLAN\n";
|
||||
}
|
||||
|
||||
@DefineSKFunction(
|
||||
name = "EdgeCaseExamples",
|
||||
description = "List a few edge case examples of plans to handle")
|
||||
public String edgeCaseExamples(
|
||||
@SKFunctionParameters(
|
||||
name = "goal",
|
||||
description = "The current goal processed by the planner",
|
||||
defaultValue = "")
|
||||
String goal,
|
||||
SKContext context) {
|
||||
return "\n"
|
||||
+ "[EXAMPLE]\n"
|
||||
+ "- List of functions:\n"
|
||||
+ "// Get the current time.\n"
|
||||
+ "TimeSkill.Time\n"
|
||||
+ "No parameters.\n"
|
||||
+ "// Write a file.\n"
|
||||
+ "FileIOSkill.WriteAsync\n"
|
||||
+ "Parameter \"path\": Destination file. (default value: sample.txt)\n"
|
||||
+ "Parameter \"content\": File content.\n"
|
||||
+ "// Makes a POST request to a uri.\n"
|
||||
+ "HttpSkill.PostAsync\n"
|
||||
+ "Parameter \"body\": The body of the request.\n"
|
||||
+ "// Read a file.\n"
|
||||
+ "FileIOSkill.ReadAsync\n"
|
||||
+ "Parameter \"path\": Source file.\n"
|
||||
+ "- End list of functions.\n"
|
||||
+ "Goal: tell me a joke.\n"
|
||||
+ "{\"plan\":{\n"
|
||||
+ "\"rationale\": \"the list does not contain functions to tell jokes or"
|
||||
+ " something funny\",\n"
|
||||
+ "\"function\": \"\",\n"
|
||||
+ "\"parameters\": {\n"
|
||||
+ "}}}\n"
|
||||
+ "#END-OF-PLAN\n";
|
||||
}
|
||||
|
||||
private String populateList(ReadOnlySkillCollection skills) {
|
||||
return skills.getAllFunctions().getAll().stream()
|
||||
.filter(skill -> !SkillName.equalsIgnoreCase(skill.getSkillName()))
|
||||
.map(
|
||||
func -> {
|
||||
StringBuilder result = new StringBuilder();
|
||||
// Function description
|
||||
if (func.getDescription() != null) {
|
||||
result.append("// " + addPeriod(func.getDescription()) + "\n");
|
||||
} else {
|
||||
LOGGER.warn(
|
||||
"{0}.{1} is missing a description",
|
||||
func.getSkillName(), func.getName());
|
||||
result.append(
|
||||
"// Function "
|
||||
+ func.getSkillName()
|
||||
+ "."
|
||||
+ func.getName()
|
||||
+ ".\n");
|
||||
}
|
||||
|
||||
// Function name
|
||||
result.append(func.getSkillName() + "." + func.getName() + "\n");
|
||||
|
||||
func.describe()
|
||||
.getParameters()
|
||||
.forEach(
|
||||
parameter -> {
|
||||
String description;
|
||||
if (parameter.getDescription() == null
|
||||
|| parameter.getDefaultValue().isEmpty()) {
|
||||
description = parameter.getName();
|
||||
} else {
|
||||
description = parameter.getDescription();
|
||||
}
|
||||
|
||||
String defaultValueString;
|
||||
if (parameter.getDefaultValue() == null
|
||||
|| parameter.getDefaultValue().isEmpty()) {
|
||||
defaultValueString = "";
|
||||
} else {
|
||||
defaultValueString =
|
||||
" (default value: "
|
||||
+ parameter.getDefaultValue()
|
||||
+ ")";
|
||||
}
|
||||
|
||||
result.append(
|
||||
"Parameter \""
|
||||
+ parameter.getName()
|
||||
+ "\": "
|
||||
+ addPeriod(description)
|
||||
+ " "
|
||||
+ defaultValueString
|
||||
+ "\n");
|
||||
});
|
||||
|
||||
return result.toString();
|
||||
})
|
||||
.reduce("", (a, b) -> a + b, (a, b) -> a + b);
|
||||
}
|
||||
|
||||
private static String addPeriod(String x) {
|
||||
return x.endsWith(".") ? x : x + ".";
|
||||
}
|
||||
}
|
||||
-588
@@ -1,588 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.actionplanner;
|
||||
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.Verify;
|
||||
import com.microsoft.semantickernel.memory.SemanticTextMemory;
|
||||
import com.microsoft.semantickernel.orchestration.AIFunction;
|
||||
import com.microsoft.semantickernel.orchestration.ContextVariables;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.orchestration.SKFunction;
|
||||
import com.microsoft.semantickernel.orchestration.WritableContextVariables;
|
||||
import com.microsoft.semantickernel.planner.PlanningException;
|
||||
import com.microsoft.semantickernel.planner.PlanningException.ErrorCodes;
|
||||
import com.microsoft.semantickernel.services.AIServiceSupplier;
|
||||
import com.microsoft.semantickernel.skilldefinition.FunctionView;
|
||||
import com.microsoft.semantickernel.skilldefinition.KernelSkillsSupplier;
|
||||
import com.microsoft.semantickernel.skilldefinition.ParameterView;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionRequestSettings;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/** Standard Semantic Kernel callable plan. Plan is used to create trees of SKFunctions. */
|
||||
public class Plan extends AIFunction<CompletionRequestSettings> {
|
||||
|
||||
private static final Pattern s_variablesRegex = Pattern.compile("\\$(\\w+)", Pattern.MULTILINE);
|
||||
|
||||
private static final String DefaultResultKey = "PLAN.RESULT";
|
||||
|
||||
// State of the plan
|
||||
private ContextVariables state;
|
||||
|
||||
// Steps of the plan
|
||||
private final List<Plan> steps = new ArrayList<>();
|
||||
|
||||
// Parameters for the plan, used to pass information to the next step
|
||||
@Nullable private ContextVariables parameters;
|
||||
|
||||
// Outputs for the plan, used to pass information to the caller
|
||||
private List<String> outputs = new ArrayList<>();
|
||||
|
||||
@Nullable private SKFunction<?> function = null;
|
||||
|
||||
public Plan(
|
||||
String goal,
|
||||
ContextVariables state,
|
||||
@Nullable KernelSkillsSupplier kernelSkillsSupplier,
|
||||
@Nullable AIServiceSupplier aiServiceSupplier) {
|
||||
super(
|
||||
new ArrayList<>(),
|
||||
Plan.class.getName(),
|
||||
"",
|
||||
goal,
|
||||
Collections.emptyList(),
|
||||
kernelSkillsSupplier,
|
||||
aiServiceSupplier);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Plan(
|
||||
String goal,
|
||||
@Nullable KernelSkillsSupplier kernelSkillsSupplier,
|
||||
@Nullable AIServiceSupplier aiServiceSupplier) {
|
||||
this(goal, SKBuilders.variables().build(), kernelSkillsSupplier, aiServiceSupplier);
|
||||
}
|
||||
|
||||
public Plan(
|
||||
SKFunction<?> function,
|
||||
ContextVariables state,
|
||||
List<String> functionOutputs,
|
||||
KernelSkillsSupplier kernelSkillsSupplier,
|
||||
AIServiceSupplier aiServiceSupplier) {
|
||||
super(
|
||||
function.describe().getParameters(),
|
||||
function.getSkillName(),
|
||||
function.getName(),
|
||||
function.getDescription(),
|
||||
formReturnParameters(functionOutputs),
|
||||
kernelSkillsSupplier,
|
||||
aiServiceSupplier);
|
||||
|
||||
this.parameters = null;
|
||||
this.function = function;
|
||||
this.outputs = new ArrayList<>(functionOutputs);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Plan(
|
||||
SKFunction<?> function,
|
||||
@Nullable ContextVariables parameters,
|
||||
ContextVariables state,
|
||||
List<String> functionOutputs,
|
||||
KernelSkillsSupplier kernelSkillsSupplier,
|
||||
AIServiceSupplier aiServiceSupplier) {
|
||||
super(
|
||||
function.describe().getParameters(),
|
||||
function.getSkillName(),
|
||||
function.getName(),
|
||||
function.getDescription(),
|
||||
formReturnParameters(functionOutputs),
|
||||
kernelSkillsSupplier,
|
||||
aiServiceSupplier);
|
||||
this.parameters = parameters;
|
||||
this.function = function;
|
||||
this.outputs = new ArrayList<>(functionOutputs);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
private static List<ParameterView> formReturnParameters(List<String> functionOutputs) {
|
||||
return functionOutputs.stream().map(ParameterView::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Plan(
|
||||
SKFunction<?> function,
|
||||
List<String> functionOutputs,
|
||||
KernelSkillsSupplier kernelSkillsSupplier,
|
||||
AIServiceSupplier aiServiceSupplier) {
|
||||
this(
|
||||
function,
|
||||
SKBuilders.variables().build(),
|
||||
functionOutputs,
|
||||
kernelSkillsSupplier,
|
||||
aiServiceSupplier);
|
||||
}
|
||||
|
||||
public Plan(
|
||||
SKFunction<?> function,
|
||||
KernelSkillsSupplier kernelSkillsSupplier,
|
||||
AIServiceSupplier aiServiceSupplier) {
|
||||
this(
|
||||
function,
|
||||
SKBuilders.variables().build(),
|
||||
new ArrayList<>(),
|
||||
kernelSkillsSupplier,
|
||||
aiServiceSupplier);
|
||||
}
|
||||
|
||||
public Plan(
|
||||
String goal,
|
||||
ContextVariables parameters,
|
||||
KernelSkillsSupplier kernelSkillsSupplier,
|
||||
AIServiceSupplier aiServiceSupplier,
|
||||
SKFunction<?>... steps) {
|
||||
this(goal, parameters, kernelSkillsSupplier, aiServiceSupplier);
|
||||
this.addSteps(steps);
|
||||
}
|
||||
|
||||
public Plan(
|
||||
String goal,
|
||||
KernelSkillsSupplier kernelSkillsSupplier,
|
||||
AIServiceSupplier aiServiceSupplier,
|
||||
SKFunction<?>... steps) {
|
||||
this(goal, kernelSkillsSupplier, aiServiceSupplier);
|
||||
this.addSteps(steps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnKernel(Kernel kernel) {
|
||||
kernel.registerSemanticFunction(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FunctionView describe() {
|
||||
if (function == null) {
|
||||
return null;
|
||||
}
|
||||
return function.describe();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Class getType() {
|
||||
if (function == null) {
|
||||
return null;
|
||||
}
|
||||
return function.getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more existing plans to the end of the current plan as steps. When you add a plan
|
||||
* as a step to the current plan, the steps of the added plan are executed after the steps of
|
||||
* the current plan have completed.
|
||||
*
|
||||
* @param steps The plans to add as steps to the current plan.
|
||||
*/
|
||||
public void addSteps(Plan... steps) {
|
||||
this.steps.addAll(Arrays.asList(steps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more new steps to the end of the current plan. When you add a new step to the
|
||||
* current plan, it is executed after the previous step in the plan has completed. Each step can
|
||||
* be a function call or another plan.
|
||||
*
|
||||
* @param steps The steps to add to the current plan.
|
||||
*/
|
||||
public void addSteps(SKFunction<?>... steps) {
|
||||
List<Plan> plans =
|
||||
Arrays.stream(steps)
|
||||
.map(step -> new Plan(step, getSkillsSupplier(), getAiServiceSupplier()))
|
||||
.collect(Collectors.toList());
|
||||
this.steps.addAll(plans);
|
||||
}
|
||||
|
||||
public void setParameters(ContextVariables functionVariables) {
|
||||
this.parameters = functionVariables;
|
||||
}
|
||||
|
||||
public void addOutputs(List<String> outputs) {
|
||||
this.outputs.addAll(outputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the next step of the plan
|
||||
*
|
||||
* @param context Context to use
|
||||
* @param settings Settings to use
|
||||
* @return The updated plan
|
||||
*/
|
||||
public Mono<SKContext> invokeNextStepAsync(
|
||||
SKContext context, @Nullable CompletionRequestSettings settings) {
|
||||
|
||||
// Execute the step
|
||||
SKContext initialContext =
|
||||
SKBuilders.context()
|
||||
.withVariables(context.getVariables().writableClone())
|
||||
.withMemory(context.getSemanticMemory())
|
||||
.withSkills(context.getSkills())
|
||||
.build();
|
||||
|
||||
return Flux.fromIterable(this.steps)
|
||||
.reduce(
|
||||
Mono.just(initialContext),
|
||||
(currentContext, step) ->
|
||||
executeStep(context, settings, currentContext, step))
|
||||
.flux()
|
||||
.concatMap(it -> it)
|
||||
.last(SKBuilders.context().build())
|
||||
.map(
|
||||
result -> {
|
||||
if (result.getVariables().get(DefaultResultKey) != null) {
|
||||
result = result.update(result.getVariables().get(DefaultResultKey));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<SKContext> executeStep(
|
||||
SKContext context,
|
||||
@Nullable CompletionRequestSettings settings,
|
||||
Mono<SKContext> currentContext2,
|
||||
Plan step) {
|
||||
return currentContext2.flatMap(
|
||||
currentContext -> {
|
||||
// Merge the state with the current context variables for step execution
|
||||
ContextVariables functionVariables =
|
||||
this.getNextStepVariables(currentContext.getVariables(), step);
|
||||
|
||||
// Execute the step
|
||||
SKContext functionContext =
|
||||
SKBuilders.context()
|
||||
.withVariables(functionVariables)
|
||||
.withMemory(context.getSemanticMemory())
|
||||
.withSkills(context.getSkills())
|
||||
.build();
|
||||
|
||||
return step.invokeAsync(functionContext, settings)
|
||||
.flatMap(
|
||||
result ->
|
||||
processInvocationResult(
|
||||
context, step, currentContext, result))
|
||||
.switchIfEmpty(
|
||||
processInvocationResult(
|
||||
context,
|
||||
step,
|
||||
currentContext,
|
||||
functionContext.copy().update("")));
|
||||
});
|
||||
}
|
||||
|
||||
private static Mono<SKContext> processInvocationResult(
|
||||
SKContext context, Plan step, SKContext currentContext, SKContext result) {
|
||||
String resultValue = result.getResult();
|
||||
|
||||
if (resultValue == null) {
|
||||
return Mono.error(
|
||||
new PlanningException(ErrorCodes.UNKNOWN_ERROR, "No result returned"));
|
||||
}
|
||||
|
||||
resultValue = resultValue.trim();
|
||||
|
||||
WritableContextVariables updatedContext = currentContext.getVariables().writableClone();
|
||||
|
||||
String finalResultValue = resultValue;
|
||||
|
||||
step.outputs.forEach(
|
||||
item -> {
|
||||
if (result.getVariables().asMap().containsKey(item)) {
|
||||
String variable = result.getVariables().get(item);
|
||||
if (variable != null) {
|
||||
updatedContext.setVariable(item, variable);
|
||||
}
|
||||
} else {
|
||||
updatedContext.setVariable(item, finalResultValue);
|
||||
}
|
||||
});
|
||||
|
||||
// If this function produces an output, don't overwrite the
|
||||
// current
|
||||
// result
|
||||
if (step.outputs.size() > 0) {
|
||||
updatedContext.setVariable(ContextVariables.MAIN_KEY, currentContext.getResult());
|
||||
} else {
|
||||
updatedContext.update(finalResultValue);
|
||||
}
|
||||
|
||||
updatedContext.setVariable(DefaultResultKey, finalResultValue);
|
||||
|
||||
return Mono.just(SKBuilders.context().clone(context).withVariables(updatedContext).build());
|
||||
}
|
||||
|
||||
public Mono<SKContext> invokeAsync(
|
||||
@Nullable String input,
|
||||
@Nullable CompletionRequestSettings settings,
|
||||
@Nullable SemanticTextMemory memory) {
|
||||
|
||||
WritableContextVariables variables = state.writableClone();
|
||||
|
||||
if (input != null) {
|
||||
variables.update(input);
|
||||
}
|
||||
|
||||
KernelSkillsSupplier skillsSupplier = getSkillsSupplier();
|
||||
|
||||
SKContext context =
|
||||
SKBuilders.context()
|
||||
.withVariables(variables)
|
||||
.withMemory(memory)
|
||||
.withSkills(skillsSupplier == null ? null : skillsSupplier.get())
|
||||
.build();
|
||||
|
||||
return this.invokeAsync(context, settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SKContext> invokeAsync(
|
||||
@Nullable String input,
|
||||
@Nullable SKContext context,
|
||||
@Nullable CompletionRequestSettings settings) {
|
||||
if (input != null) {
|
||||
this.state = state.writableClone().update(input);
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
context =
|
||||
SKBuilders.context()
|
||||
.withVariables(state)
|
||||
.withSkills(
|
||||
getSkillsSupplier() == null ? null : getSkillsSupplier().get())
|
||||
.build();
|
||||
} else {
|
||||
ContextVariables variables = context.getVariables().writableClone().update(state, true);
|
||||
context = context.update(variables);
|
||||
}
|
||||
|
||||
return super.invokeAsync(input, context, settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<SKContext> invokeAsyncInternal(
|
||||
SKContext contextIn, @Nullable CompletionRequestSettings settings) {
|
||||
if (function != null) {
|
||||
return ((SKFunction) function).invokeAsync(contextIn, settings);
|
||||
} else {
|
||||
// loop through steps and execute until completion
|
||||
SKContext c = addVariablesToContext(this.state, contextIn);
|
||||
return this.invokeNextStepAsync(c, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand variables in the input string.
|
||||
*
|
||||
* @param variables Variables to use for expansion.
|
||||
* @param input Input string to expand.
|
||||
* @return Expanded string.
|
||||
*/
|
||||
private String expandFromVariables(ContextVariables variables, String input) {
|
||||
String result = input;
|
||||
Matcher matches = s_variablesRegex.matcher(input);
|
||||
while (matches.find()) {
|
||||
|
||||
String varName = matches.group(1);
|
||||
|
||||
String value = variables.get(varName);
|
||||
|
||||
if (value == null) {
|
||||
value = state.get(varName);
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
|
||||
result = result.replaceAll("\\$" + varName, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Add any missing variables from a plan state variables to the context. */
|
||||
private static SKContext addVariablesToContext(ContextVariables vars, SKContext context) {
|
||||
WritableContextVariables clone = context.getVariables().writableClone();
|
||||
vars.asMap()
|
||||
.forEach(
|
||||
(key, value) -> {
|
||||
if (Verify.isNullOrEmpty(clone.get(key))) {
|
||||
clone.setVariable(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return SKBuilders.context().clone(context).withVariables(clone).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the variables for the next step in the plan.
|
||||
*
|
||||
* @param variables The current context variables.
|
||||
* @param step The next step in the plan.
|
||||
* @return The context variables for the next step in the plan.
|
||||
*/
|
||||
private ContextVariables getNextStepVariables(ContextVariables variables, Plan step) {
|
||||
// Priority for Input
|
||||
// - Parameters (expand from variables if needed)
|
||||
// - SKContext.Variables
|
||||
// - Plan.State
|
||||
// - Empty if sending to another plan
|
||||
// - Plan.Description
|
||||
|
||||
String input = "";
|
||||
if (step.parameters != null
|
||||
&& !Verify.isNullOrEmpty(step.parameters.getInput())
|
||||
&& !step.parameters.getInput().equals(SKFunctionParameters.NO_DEFAULT_VALUE)) {
|
||||
input =
|
||||
this.expandFromVariables(
|
||||
variables, Objects.requireNonNull(step.parameters.getInput()));
|
||||
} else if (!Verify.isNullOrEmpty(variables.getInput())) {
|
||||
input = Objects.requireNonNull(variables.getInput());
|
||||
} else if (!Verify.isNullOrEmpty(this.state.getInput())) {
|
||||
input = Objects.requireNonNull(this.state.getInput());
|
||||
} else if (steps.size() > 0) {
|
||||
input = "";
|
||||
} else if (!Verify.isNullOrEmpty(this.getDescription())) {
|
||||
input = this.getDescription();
|
||||
}
|
||||
|
||||
WritableContextVariables stepVariables =
|
||||
SKBuilders.variables()
|
||||
.withInput(Objects.requireNonNull(input))
|
||||
.build()
|
||||
.writableClone();
|
||||
|
||||
// Priority for remaining stepVariables is:
|
||||
// - Function Parameters (pull from variables or state by a key value)
|
||||
// - Step Parameters (pull from variables or state by a key value)
|
||||
FunctionView functionParameters = step.describe();
|
||||
if (functionParameters != null) {
|
||||
for (ParameterView param : functionParameters.getParameters()) {
|
||||
if (param.getName().equals(ContextVariables.MAIN_KEY)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Verify.isNullOrEmpty(variables.get(param.getName()))) {
|
||||
String variable = Objects.requireNonNull(variables.get(param.getName()));
|
||||
stepVariables.setVariable(param.getName(), variable);
|
||||
} else if (!Verify.isNullOrEmpty(this.state.get(param.getName()))) {
|
||||
String variable = Objects.requireNonNull(this.state.get(param.getName()));
|
||||
stepVariables.setVariable(param.getName(), variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step.parameters != null) {
|
||||
step.parameters
|
||||
.asMap()
|
||||
.forEach(
|
||||
(key, value) -> {
|
||||
// Don't overwrite variable values that are already set
|
||||
if (!Verify.isNullOrEmpty(stepVariables.get(key))) {
|
||||
return;
|
||||
}
|
||||
|
||||
String expandedValue = this.expandFromVariables(variables, value);
|
||||
if (expandedValue != null
|
||||
&& !expandedValue.equalsIgnoreCase(value)) {
|
||||
stepVariables.setVariable(key, expandedValue);
|
||||
} else if (!Verify.isNullOrEmpty(variables.get(key))) {
|
||||
String variable = variables.get(key);
|
||||
if (variable != null) {
|
||||
stepVariables.setVariable(key, variable);
|
||||
}
|
||||
} else if (!Verify.isNullOrEmpty(state.get(key))) {
|
||||
String variable = state.get(key);
|
||||
if (variable != null) {
|
||||
stepVariables.setVariable(key, variable);
|
||||
}
|
||||
} else {
|
||||
if (expandedValue != null) {
|
||||
stepVariables.setVariable(key, expandedValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return stepVariables;
|
||||
}
|
||||
|
||||
public String toPlanString() {
|
||||
return toPlanString(" ");
|
||||
}
|
||||
|
||||
private String toPlanString(String indent) {
|
||||
String goalHeader = indent + "Goal: " + getDescription() + "\n\n" + indent + "Steps:\n";
|
||||
|
||||
String stepItems =
|
||||
steps.stream()
|
||||
.map(step -> stepToString(indent, step))
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
return goalHeader + stepItems;
|
||||
}
|
||||
|
||||
private static String stepToString(String indent, Plan step) {
|
||||
if (step.steps.size() == 0) {
|
||||
String skillName = step.getSkillName();
|
||||
String stepName = step.getName();
|
||||
|
||||
String parameters =
|
||||
step.getParametersView().stream()
|
||||
.map(
|
||||
param -> {
|
||||
String value = param.getDefaultValue();
|
||||
if (step.getParameters() != null
|
||||
&& step.getParameters().get(param.getName())
|
||||
!= null) {
|
||||
value = step.getParameters().get(param.getName());
|
||||
}
|
||||
return "\t" + param.getName() + ": \"" + value + "\"";
|
||||
})
|
||||
.collect(Collectors.joining(" "));
|
||||
|
||||
if (!Verify.isNullOrEmpty(parameters)) {
|
||||
parameters = " " + parameters;
|
||||
}
|
||||
|
||||
String outputs = step.outputs.stream().findFirst().orElse("");
|
||||
if (!Verify.isNullOrEmpty(outputs)) {
|
||||
outputs = " => " + outputs;
|
||||
}
|
||||
|
||||
return indent
|
||||
+ indent
|
||||
+ "- "
|
||||
+ String.join(".", skillName, stepName)
|
||||
+ "\t\t\t"
|
||||
+ parameters
|
||||
+ outputs;
|
||||
}
|
||||
|
||||
return step.toPlanString(indent + indent);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ContextVariables getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
-256
@@ -1,256 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.sequentialplanner;
|
||||
|
||||
import com.microsoft.semantickernel.memory.MemoryQueryResult;
|
||||
import com.microsoft.semantickernel.memory.NullMemory;
|
||||
import com.microsoft.semantickernel.memory.SemanticTextMemory;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.orchestration.SKFunction;
|
||||
import com.microsoft.semantickernel.skilldefinition.ReadOnlySkillCollection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class DefaultSequentialPlannerSKContext {
|
||||
|
||||
public static final String PlannerMemoryCollectionName = "Planning.SKFunctionsManual";
|
||||
|
||||
public static final String PlanSKFunctionsAreRemembered = "Planning.SKFunctionsAreRemembered";
|
||||
private final SKContext delegate;
|
||||
|
||||
public DefaultSequentialPlannerSKContext(SKContext delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the manual for all available functions.
|
||||
*
|
||||
* @param semanticQuery The semantic query for finding relevant registered functions
|
||||
* @param config The planner skill config
|
||||
* @param includeFunctionOutputs Whether to include function outputs in the manual
|
||||
* @return A string containing the manual for all available functions.
|
||||
*/
|
||||
public Mono<String> getFunctionsManualAsync(
|
||||
@Nullable String semanticQuery,
|
||||
@Nullable SequentialPlannerRequestSettings config,
|
||||
boolean includeFunctionOutputs) {
|
||||
if (config == null) {
|
||||
config = new SequentialPlannerRequestSettings();
|
||||
}
|
||||
|
||||
Mono<SortedSet<SKFunction<?>>> functions =
|
||||
getAvailableFunctionsAsync(config, semanticQuery);
|
||||
|
||||
return functions.map(
|
||||
funcs ->
|
||||
funcs.stream()
|
||||
.map(it -> it.toManualString(includeFunctionOutputs))
|
||||
.collect(Collectors.joining("\n\n")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of functions that are available to the user based on the semantic query and
|
||||
// the excluded skills and functions.
|
||||
/// </summary>
|
||||
/// <param name="context">The SKContext</param>
|
||||
/// <param name="config">The planner config.</param>
|
||||
/// <param name="semanticQuery">The semantic query for finding relevant registered
|
||||
// functions</param>
|
||||
/// <returns>A list of functions that are available to the user based on the semantic query and
|
||||
// the excluded skills and functions.</returns>
|
||||
public Mono<SortedSet<SKFunction<?>>> getAvailableFunctionsAsync(
|
||||
SequentialPlannerRequestSettings config, @Nullable String semanticQuery) {
|
||||
Set<String> excludedSkills = config.getExcludedSkills();
|
||||
Set<String> excludedFunctions = config.getExcludedFunctions();
|
||||
Set<String> includedFunctions = config.getIncludedFunctions();
|
||||
|
||||
// context.ThrowIfSkillCollectionNotSet();
|
||||
|
||||
ReadOnlySkillCollection skills = delegate.getSkills();
|
||||
List<SKFunction<?>> functions;
|
||||
if (skills != null) {
|
||||
functions = skills.getAllFunctions().getAll();
|
||||
} else {
|
||||
functions = Collections.emptyList();
|
||||
}
|
||||
|
||||
List<SKFunction<?>> availableFunctions =
|
||||
functions.stream()
|
||||
/*
|
||||
// Is there any function that should be excluded?
|
||||
.filter(
|
||||
it ->
|
||||
it instanceof SemanticSKFunction
|
||||
|| it instanceof NativeSKFunction)
|
||||
|
||||
*/
|
||||
.filter(
|
||||
s ->
|
||||
!excludedSkills.contains(s.getSkillName())
|
||||
&& !excludedFunctions.contains(s.getName()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Comparator<SKFunction<?>> comparator =
|
||||
Comparator.<SKFunction<?>, String>comparing(SKFunction::getSkillName)
|
||||
.thenComparing(SKFunction::getName);
|
||||
|
||||
if (semanticQuery == null
|
||||
|| semanticQuery.isEmpty()
|
||||
|| delegate.getSemanticMemory() == null
|
||||
|| delegate.getSemanticMemory() instanceof NullMemory
|
||||
|| config.getRelevancyThreshold() == null) {
|
||||
// If no semantic query is provided, return all available functions.
|
||||
// If a Memory provider has not been registered, return all available functions.
|
||||
TreeSet<SKFunction<?>> result = new TreeSet<>(comparator);
|
||||
result.addAll(availableFunctions);
|
||||
return Mono.just(result);
|
||||
} else {
|
||||
// Remember functions in memory so that they can be searched.
|
||||
return rememberFunctionsAsync(availableFunctions)
|
||||
.flatMap(
|
||||
updatedContext -> {
|
||||
SemanticTextMemory updatedMemory =
|
||||
updatedContext.getSemanticMemory();
|
||||
if (updatedMemory == null) {
|
||||
return Mono.just(new ArrayList<SKFunction<Void>>());
|
||||
}
|
||||
// Search for functions that match the semantic query.
|
||||
return updatedMemory
|
||||
.searchAsync(
|
||||
PlannerMemoryCollectionName,
|
||||
semanticQuery,
|
||||
config.getMaxRelevantFunctions(),
|
||||
config.getRelevancyThreshold(),
|
||||
false)
|
||||
.flatMap(
|
||||
memories ->
|
||||
getRelevantFunctionsAsync(
|
||||
availableFunctions, memories));
|
||||
})
|
||||
.map(
|
||||
memories -> {
|
||||
List<String> added =
|
||||
memories.stream()
|
||||
.map(SKFunction::getName)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<SKFunction<?>> missingFunctions =
|
||||
getMissingFunctions(
|
||||
includedFunctions, availableFunctions, added);
|
||||
|
||||
ArrayList<SKFunction<?>> res = new ArrayList<>(memories);
|
||||
res.addAll(missingFunctions);
|
||||
return res;
|
||||
})
|
||||
.map(
|
||||
res -> {
|
||||
TreeSet<SKFunction<?>> result = new TreeSet<>(comparator);
|
||||
result.addAll(availableFunctions);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SKFunction<?>> getMissingFunctions(
|
||||
Set<String> includedFunctions,
|
||||
List<SKFunction<?>> availableFunctions,
|
||||
List<String> added) {
|
||||
return includedFunctions.stream()
|
||||
.filter(func -> !added.contains(func))
|
||||
.flatMap(
|
||||
missing ->
|
||||
availableFunctions.stream()
|
||||
.filter(it -> it.getName().equals(missing)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Mono<? extends List<? extends SKFunction<?>>> getRelevantFunctionsAsync(
|
||||
List<SKFunction<?>> availableFunctions, List<MemoryQueryResult> memories) {
|
||||
return Flux.fromIterable(memories)
|
||||
.map(
|
||||
memoryEntry ->
|
||||
availableFunctions.stream()
|
||||
.filter(
|
||||
it ->
|
||||
Objects.equals(
|
||||
it.toFullyQualifiedName(),
|
||||
memoryEntry.getMetadata().getId()))
|
||||
.findFirst()
|
||||
.orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.collectList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves all available functions to memory.
|
||||
/// </summary>
|
||||
/// <param name="context">The SKContext to save the functions to.</param>
|
||||
/// <param name="availableFunctions">The available functions to save.</param>
|
||||
Mono<SKContext> rememberFunctionsAsync(List<SKFunction<?>> availableFunctions) {
|
||||
// Check if the functions have already been saved to memory.
|
||||
if (delegate.getVariables().asMap().containsKey(PlanSKFunctionsAreRemembered)) {
|
||||
return Mono.just(delegate);
|
||||
}
|
||||
|
||||
SemanticTextMemory memory = delegate.getSemanticMemory();
|
||||
|
||||
if (memory == null) {
|
||||
delegate.setVariable(PlanSKFunctionsAreRemembered, "true");
|
||||
return Mono.just(delegate);
|
||||
}
|
||||
|
||||
return Flux.fromIterable(availableFunctions)
|
||||
.concatMap(
|
||||
function -> {
|
||||
String functionName = function.toFullyQualifiedName();
|
||||
String key = functionName;
|
||||
String description =
|
||||
function.getDescription() == null
|
||||
|| function.getDescription().isEmpty()
|
||||
? functionName
|
||||
: function.getDescription();
|
||||
String textToEmbed = function.toEmbeddingString();
|
||||
|
||||
// It'd be nice if there were a saveIfNotExists method on the memory
|
||||
// interface
|
||||
return memory.getAsync(PlannerMemoryCollectionName, key, false)
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(
|
||||
memoryEntry -> {
|
||||
// TODO It'd be nice if the minRelevanceScore could
|
||||
// be a parameter for each item that was saved to
|
||||
// memory
|
||||
// As folks may want to tune their functions to be
|
||||
// more or less relevant.
|
||||
// Memory now supports these such strategies.
|
||||
return memory.saveInformationAsync(
|
||||
PlannerMemoryCollectionName,
|
||||
textToEmbed,
|
||||
key,
|
||||
description,
|
||||
"");
|
||||
})
|
||||
.flatMap(
|
||||
newKey ->
|
||||
memory.getAsync(
|
||||
PlannerMemoryCollectionName,
|
||||
newKey,
|
||||
true));
|
||||
})
|
||||
.ignoreElements()
|
||||
.map(
|
||||
newMemory -> {
|
||||
delegate.setVariable(PlanSKFunctionsAreRemembered, "true");
|
||||
return delegate;
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user