Java - Add ChatCompletions support (#1221)

### Motivation and Context
<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->


### Description
<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->


### Contribution Checklist
<!-- Before submitting this PR, please make sure: -->
- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows SK Contribution Guidelines
(https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
- [ ] The code follows the .NET coding conventions
(https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions)
verified with `dotnet format`
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
This commit is contained in:
John Oliver
2023-06-07 17:45:02 +01:00
committed by GitHub
parent 95412ab95a
commit a720809a89
48 changed files with 2006 additions and 616 deletions
+47 -10
View File
@@ -1,24 +1,61 @@
name: Build Java Semantic Kernel
on:
workflow_dispatch:
push:
branches: [ "experimental-java", "java-dev" ]
branches: [ "experimental-java" ]
paths:
- 'java/**'
pull_request:
branches: [ "experimental-java", "java-dev" ]
branches: [ "experimental-java" ]
paths:
- 'java/**'
permissions:
contents: read
jobs:
build:
java-build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java-versions: [8, 17]
name: Java CI on JDK${{ matrix.java-versions }}
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
- name: Checkout
uses: actions/checkout@v3
# Need to use JDK 11 to build for JDK 8
- name: Set JDK
id: set-jdk
shell: bash
run: |
if [[ ${{ matrix.java-versions }} == 8 ]]; then
echo "JDK_VERSION=11" >> $GITHUB_OUTPUT
else
version=${{ matrix.java-versions }}
echo "JDK_VERSION=$version" >> $GITHUB_OUTPUT
fi
- uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
java-version: ${{ steps.set-jdk.outputs.JDK_VERSION }}
distribution: microsoft
cache: maven
- name: Build with Maven
run: cd java && ./mvnw -B -Pbug-check clean install --file pom.xml
run: ./mvnw -B -Pbug-check -DskipTests -Pcompile-jdk${{ matrix.java-versions }} clean install --file pom.xml
working-directory: java
- name: Run tests
run: ./mvnw -B -Pbug-check -Pcompile-jdk${{ matrix.java-versions }} test --file pom.xml
working-directory: java
- uses: actions/upload-artifact@v2
if: always()
with:
name: test_output_sk_jdk${{ matrix.java-versions }}u
path: java/**/target/surefire-reports/*Test.txt
+80
View File
@@ -0,0 +1,80 @@
name: Java format automation
on:
issue_comment:
types: [ created ]
jobs:
comment-driven-automation:
if: |
github.event.issue.pull_request &&
(
startsWith(github.event.comment.body, '/spotless') ||
startsWith(github.event.comment.body, '/help')
)
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Get command
env:
BODY: ${{ github.event.comment.body }}
run: |
# intentionally only looking at the first line of the body
command=$(echo "$BODY" | head -1 | sed "s;^/;;")
echo "COMMAND=$command" >> $GITHUB_ENV
- uses: actions/checkout@v3
- name: Check out PR branch
env:
NUMBER: ${{ github.event.issue.number }}
GH_TOKEN: ${{ github.token }}
run: |
gh pr checkout $NUMBER
if: env.COMMAND == 'spotless'
- name: Set up Maven cache
uses: actions/setup-java@v3
with:
java-version: 17
distribution: microsoft
cache: maven
if: env.COMMAND == 'spotless'
- name: Set git user
run: |
git config user.name github-actions[bot]
git config user.email github-action[bot]@users.noreply.github.com
if: env.COMMAND == 'spotless'
- name: Run command
env:
NUMBER: ${{ github.event.issue.number }}
GH_TOKEN: ${{ github.token }}
run: |
available_commands="Available commands:
* \`/spotless\` - runs \`./mvnw process-sources -DskipTests -Pbug-check\`
* \`/help\` - displays available commands
"
if [[ "$COMMAND" == "spotless" ]]; then
./mvnw process-sources -DskipTests -Pbug-check
if git diff --quiet; then
gh pr comment $NUMBER --body "Already up-to-date"
exit 0 # success
fi
git commit -a -m "./mvnw process-sources -DskipTests -Pbug-check"
git push
elif [[ "$COMMAND" == "help" ]]; then
gh pr comment $NUMBER --body "$available_commands"
else
body="Unknown command: \`$COMMAND\`
$available_commands
"
gh pr comment $NUMBER --body "$body"
fi
working-directory: java
+52 -14
View File
@@ -1,30 +1,68 @@
name: Run Java Integration Tests and Samples
on:
workflow_dispatch:
push:
branches: [ "experimental-java", "java-dev" ]
branches: [ "experimental-java" ]
paths:
- 'java/**'
pull_request:
branches: [ "experimental-java", "java-dev" ]
branches: [ "experimental-java" ]
paths:
- 'java/**'
permissions:
contents: read
jobs:
build:
java-integration-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java-versions: [8, 17]
name: Java Tests on JDK${{ matrix.java-versions }}
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
- name: Checkout
uses: actions/checkout@v3
# Need to use JDK 11 to build for JDK 8
- name: Set JDK
id: set-jdk
shell: bash
run: |
if [[ ${{ matrix.java-versions }} == 8 ]]; then
echo "JDK_VERSION=11" >> $GITHUB_OUTPUT
else
version=${{ matrix.java-versions }}
echo "JDK_VERSION=$version" >> $GITHUB_OUTPUT
fi
- uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
java-version: ${{ steps.set-jdk.outputs.JDK_VERSION }}
distribution: microsoft
cache: maven
- name: Build with Maven
run: cd java && ./mvnw -B -DskipTests -Pbug-check clean install --file pom.xml
run: ./mvnw -B -Pbug-check -DskipTests -Pcompile-jdk${{ matrix.java-versions }} clean install --file pom.xml
working-directory: java
- name: Run Tests
run: cd java/api-test && ../mvnw -B -Pbug-check clean install --file pom.xml
# API tests run on JDK 17+
- name: Run integration tests
run: ../mvnw -B -Pbug-check clean install --file pom.xml
working-directory: java/api-test
env:
OPENAI_API_KEY: ""
AZURE_OPENAI_ENDPOINT: ""
AZURE_OPENAI_API_KEY: ""
AZURE_OPENAI_DEPLOYMENT_NAME: ""
if: ${{ matrix.java-versions >= 17 }}
- name: Build samples
run: cd java/samples && ../mvnw -B clean install --file pom.xml
# Samples build on JDK 17+
- name: Build semantic-kernel samples
run: ../mvnw -B clean install --file pom.xml
working-directory: java/samples
if: ${{ matrix.java-versions >= 17 }}
@@ -13,7 +13,6 @@ import com.microsoft.semantickernel.memory.SemanticTextMemory;
import com.microsoft.semantickernel.memory.VolatileMemoryStore;
import com.microsoft.semantickernel.textcompletion.CompletionSKContext;
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
+17 -2
View File
@@ -519,13 +519,28 @@
</repositories>
<profiles>
<profile>
<id>compile-jdk8</id>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</profile>
<profile>
<id>compile-jdk17</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</profile>
<profile>
<id>bug-check</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
+11 -99
View File
@@ -1,13 +1,18 @@
<?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">
<?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>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>samples</artifactId>
<version>0.2.0-SNAPSHOT</version>
<!-- TODO: Consider inheriting from parent -->
<packaging>pom</packaging>
<modules>
<module>sample-code</module>
<module>semantickernel-guice</module>
</modules>
<properties>
<com.uber.nullaway.version>0.10.10</com.uber.nullaway.version>
@@ -29,97 +34,4 @@
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>semantickernel-api</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel.connectors</groupId>
<artifactId>semantickernel-connectors</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>openai-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>semantickernel-core</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>semantickernel-core-skills</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel.extensions</groupId>
<artifactId>sequentialplanner-extensions</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>bug-check</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>{maven.compiler-plugin.version}</version>
<configuration>
<source>${maven.compiler.release}</source>
<target>${maven.compiler.release}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<showWarnings>true</showWarnings>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<!-- Remove exclusions when more project is stable -->
<arg>-Xplugin:ErrorProne
-XepOpt:NullAway:AnnotatedPackages=com.microsoft.semantickernel
-Xep:AlmostJavadoc:OFF -Xep:MissingSummary:OFF
-Xep:UnusedVariable:OFF -Xep:EmptyBlockTag:OFF
</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${google.errorprone.core.version}</version>
</path>
<path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>${com.uber.nullaway.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
</project>
+65
View File
@@ -0,0 +1,65 @@
<?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.semantickernel</groupId>
<artifactId>samples</artifactId>
<version>0.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>sample-code</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>semantickernel-api</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel.connectors</groupId>
<artifactId>semantickernel-connectors</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>openai-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>semantickernel-core</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>semantickernel-core-skills</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel.extensions</groupId>
<artifactId>sequentialplanner-extensions</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -6,10 +6,7 @@ import com.microsoft.semantickernel.Config;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.KernelConfig;
import com.microsoft.semantickernel.builders.SKBuilders;
import com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion;
import com.microsoft.semantickernel.connectors.ai.openai.textembeddings.OpenAITextEmbeddingGeneration;
import com.microsoft.semantickernel.coreskills.TimeSkill;
import com.microsoft.semantickernel.memory.NullMemory;
import com.microsoft.semantickernel.orchestration.SKContext;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
import com.microsoft.semantickernel.templateengine.PromptTemplateEngine;
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.syntaxexamples;
import com.microsoft.openai.OpenAIAsyncClient;
import com.microsoft.semantickernel.Config;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.KernelConfig;
import com.microsoft.semantickernel.builders.SKBuilders;
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatHistory;
/**
* The following example shows how to use Semantic Kernel with OpenAI ChatGPT API
*/
public class Example17_ChatGPT {
public static void main(String[] args) {
OpenAIAsyncClient client = Config.getClient(true);
KernelConfig kernelConfig = SKBuilders.kernelConfig()
.addChatCompletionService(
"chat-test",
kernel -> SKBuilders.chatCompletion().build(client, "chat-test")
)
.build();
Kernel kernel = SKBuilders.kernel().setKernelConfig(kernelConfig).build();
ChatCompletion<OpenAIChatHistory> chatGPT = kernel.getService(null, ChatCompletion.class);
OpenAIChatHistory chatHistory = chatGPT.createNewChat("You are a librarian, expert about books");
// First user message
chatHistory.addUserMessage("Hi, I'm looking for book suggestions");
messageOutputAsync(chatHistory);
// First bot assistant message
String reply = chatGPT.generateMessageAsync(chatHistory, null).block();
chatHistory.addAssistantMessage(reply);
messageOutputAsync(chatHistory);
// Second user message
chatHistory.addUserMessage("I love history and philosophy, I'd like to learn something new about Greece, any suggestion?");
messageOutputAsync(chatHistory);
// Second bot assistant message
reply = chatGPT.generateMessageAsync(chatHistory, null).block();
chatHistory.addAssistantMessage(reply);
messageOutputAsync(chatHistory);
}
private static void messageOutputAsync(ChatHistory chatHistory) {
ChatHistory.Message message = chatHistory.getMessages().get(chatHistory.getMessages().size() - 1);
System.out.println(message.getAuthorRoles() + ": " + message.getContent());
System.out.println("------------------------");
}
}
+50
View File
@@ -0,0 +1,50 @@
<?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.semantickernel</groupId>
<artifactId>samples</artifactId>
<version>0.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>semantickernel-guice</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>semantickernel-core</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel.connectors</groupId>
<artifactId>semantickernel-connectors</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantickernel</groupId>
<artifactId>semantickernel-core-skills</artifactId>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>7.0.0</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-assistedinject</artifactId>
<version>7.0.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.guice;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
import jakarta.inject.Inject;
public interface CompletionFunctionFactory {
CompletionSKFunction createFunction(
String prompt, String name, PromptTemplateConfig.CompletionConfig completionConfig);
class CompletionFunctionFactoryImpl implements CompletionFunctionFactory {
@Inject private Kernel kernel;
@Override
public CompletionSKFunction createFunction(
String prompt,
String name,
PromptTemplateConfig.CompletionConfig completionConfig) {
return kernel.getSemanticFunctionBuilder()
.createFunction(prompt, name, null, null, completionConfig);
}
}
}
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.guice;
import com.azure.core.credential.AzureKeyCredential;
import com.microsoft.openai.AzureOpenAIClient;
import com.microsoft.openai.OpenAIAsyncClient;
import com.microsoft.openai.OpenAIClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
// TODO Remove when the upstream configuration updates arrive
public class Config {
private static final Logger LOGGER = LoggerFactory.getLogger(Config.class);
public static final String OPEN_AI_CONF_PROPERTIES =
"samples/java/semantickernel-samples/conf.openai.properties";
public static final String AZURE_CONF_PROPERTIES =
"samples/java/semantickernel-samples/conf.azure.properties";
public static String getOpenAIKey(String conf) {
return getConfigValue(conf, "token");
}
public static String getAzureOpenAIEndpoint(String conf) {
return getConfigValue(conf, "endpoint");
}
private static String getConfigValue(String configFile, String propertyName) {
File config = new File(configFile);
try (FileInputStream fis = new FileInputStream(config.getAbsolutePath())) {
Properties props = new Properties();
props.load(fis);
return props.getProperty(propertyName);
} catch (IOException e) {
LOGGER.error(
"Unable to load config value " + propertyName + " from file" + configFile, e);
throw new RuntimeException(configFile + " not configured properly");
}
}
/**
* Returns the client that will handle AzureOpenAI or OpenAI requests.
*
* @param useAzureOpenAI whether to use AzureOpenAI or OpenAI.
* @return client to be used by the kernel.
*/
public static OpenAIAsyncClient getClient(boolean useAzureOpenAI) {
if (useAzureOpenAI) {
return new AzureOpenAIClient(
new com.azure.ai.openai.OpenAIClientBuilder()
.endpoint(Config.getAzureOpenAIEndpoint(AZURE_CONF_PROPERTIES))
.credential(
new AzureKeyCredential(
Config.getOpenAIKey(AZURE_CONF_PROPERTIES)))
.buildAsyncClient());
}
return new OpenAIClientBuilder()
.setApiKey(Config.getOpenAIKey(OPEN_AI_CONF_PROPERTIES))
.build();
}
}
@@ -0,0 +1,131 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.guice;
import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.google.inject.util.Providers;
import com.microsoft.openai.OpenAIAsyncClient;
import com.microsoft.semantickernel.DefaultKernel;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.KernelConfig;
import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration;
import com.microsoft.semantickernel.builders.SKBuilders;
import com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion;
import com.microsoft.semantickernel.connectors.ai.openai.textembeddings.OpenAITextEmbeddingGeneration;
import com.microsoft.semantickernel.memory.NullMemory;
import com.microsoft.semantickernel.memory.SemanticTextMemory;
import com.microsoft.semantickernel.skilldefinition.DefaultSkillCollection;
import com.microsoft.semantickernel.skilldefinition.ReadOnlySkillCollection;
import com.microsoft.semantickernel.templateengine.DefaultPromptTemplateEngine;
import com.microsoft.semantickernel.templateengine.PromptTemplateEngine;
import javax.annotation.Nullable;
public class SemanticKernelModule extends AbstractModule {
private final SemanticTextMemory memory;
private final KernelConfig.Builder kernelConfig;
private final ReadOnlySkillCollection skillCollection;
private final PromptTemplateEngine promptTemplateEngine;
private String textCompletionModelId;
private boolean withMemory;
private String embeddingsGenerationServiceId;
private OpenAIAsyncClient client;
public SemanticKernelModule(
KernelConfig.Builder kernelConfig,
@Nullable SemanticTextMemory memory,
@Nullable ReadOnlySkillCollection skillCollection,
@Nullable PromptTemplateEngine promptTemplateEngine) {
this.kernelConfig = kernelConfig;
if (memory != null) {
this.memory = memory.copy();
} else {
this.memory = null;
}
if (skillCollection == null) {
skillCollection = new DefaultSkillCollection();
}
this.skillCollection = skillCollection;
if (promptTemplateEngine == null) {
promptTemplateEngine = new DefaultPromptTemplateEngine();
}
this.promptTemplateEngine = promptTemplateEngine;
}
public SemanticKernelModule(
KernelConfig.Builder kernelConfig, @Nullable SemanticTextMemory memory) {
this(kernelConfig, memory, null, null);
}
public SemanticKernelModule(KernelConfig.Builder kernelConfig) {
this(kernelConfig, null);
}
public SemanticKernelModule() {
this(SKBuilders.kernelConfig(), null);
}
public SemanticKernelModule withTextCompletionService(String model) {
this.textCompletionModelId = model;
return this;
}
public SemanticKernelModule withClient(OpenAIAsyncClient client) {
this.client = client;
return this;
}
@Override
protected void configure() {
super.configure();
if (client == null) {
client = Config.getClient(true);
}
bind(OpenAIAsyncClient.class).toInstance(client);
if (textCompletionModelId != null) {
kernelConfig.addTextCompletionService(
textCompletionModelId,
kernel -> new OpenAITextCompletion(client, textCompletionModelId));
}
if (embeddingsGenerationServiceId != null) {
OpenAITextEmbeddingGeneration embeddings =
new OpenAITextEmbeddingGeneration(client, embeddingsGenerationServiceId);
kernelConfig.addTextEmbeddingsGenerationService(
embeddingsGenerationServiceId, kernel -> embeddings);
bind(new TypeLiteral<EmbeddingGeneration<String, Float>>() {}).toInstance(embeddings);
}
bind(KernelConfig.class).toInstance(kernelConfig.build());
bind(PromptTemplateEngine.class).toInstance(promptTemplateEngine);
bind(ReadOnlySkillCollection.class).toProvider(Providers.of(skillCollection));
if (withMemory) {
bind(SemanticTextMemory.class).toProvider(Providers.of(memory));
} else {
bind(SemanticTextMemory.class).toInstance(NullMemory.getInstance());
}
bind(Kernel.class).to(DefaultKernel.class);
bind(CompletionFunctionFactory.class)
.to(CompletionFunctionFactory.CompletionFunctionFactoryImpl.class);
}
public SemanticKernelModule withMemory() {
this.withMemory = true;
return this;
}
public SemanticKernelModule withTextEmbeddingsGenerationService(
String embeddingsGenerationServiceId) {
this.embeddingsGenerationServiceId = embeddingsGenerationServiceId;
return this;
}
}
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.guice;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
import com.microsoft.semantickernel.textcompletion.CompletionSKContext;
import java.util.ArrayList;
public class CompletionFunctionExample {
private final CompletionFunctionFactory completionFunctionFactory;
public static void main(String[] args) {
Injector injector =
Guice.createInjector(
new SemanticKernelModule().withTextCompletionService("text-davinci-003"));
CompletionFunctionExample example = injector.getInstance(CompletionFunctionExample.class);
example.run();
}
@Inject
public CompletionFunctionExample(CompletionFunctionFactory completionFunctionFactory) {
this.completionFunctionFactory = completionFunctionFactory;
}
public void run() {
String prompt = "{{$input}}\n" + "Summarize the content above.";
String text =
"\n"
+ "Demo (ancient Greek poet)\n"
+ "From Wikipedia, the free encyclopedia\n"
+ "Demo or Damo (Greek: Δεμώ, Δαμώ; fl. c. AD 200) was a Greek woman of the\n"
+ "Roman period, known for a single epigram, engraved upon the Colossus of\n"
+ "Memnon, which bears her name. She speaks of herself therein as a lyric\n"
+ "poetess dedicated to the Muses, but nothing is known of her life.[1]\n"
+ "Identity\n"
+ "Demo was evidently Greek, as her name, a traditional epithet of Demeter,\n"
+ "signifies. The name was relatively common in the Hellenistic world, in\n"
+ "Egypt and elsewhere, and she cannot be further identified. The date of her\n"
+ "visit to the Colossus of Memnon cannot be established with certainty, but\n"
+ "internal evidence on the left leg suggests her poem was inscribed there at\n"
+ "some point in or after AD 196.[2]\n"
+ "Epigram\n"
+ "There are a number of graffiti inscriptions on the Colossus of Memnon.\n"
+ "Following three epigrams by Julia Balbilla, a fourth epigram, in elegiac\n"
+ "couplets, entitled and presumably authored by \"Demo\" or \"Damo\" (the\n"
+ "Greek inscription is difficult to read), is a dedication to the Muses.[2]\n"
+ "The poem is traditionally published with the works of Balbilla, though the\n"
+ "internal evidence suggests a different author.[1]\n"
+ "In the poem, Demo explains that Memnon has shown her special respect. In\n"
+ "return, Demo offers the gift for poetry, as a gift to the hero. At the end\n"
+ "of this epigram, she addresses Memnon, highlighting his divine status by\n"
+ "recalling his strength and holiness.[2]\n"
+ "Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic\n"
+ "dialect. The language indicates she was knowledgeable in Homeric\n"
+ "poetry—'bearing a pleasant gift', for example, alludes to the use of that\n"
+ "phrase throughout the Iliad and Odyssey.[a][2];";
CompletionSKContext summary =
completionFunctionFactory
.createFunction(
prompt,
"summarize",
new PromptTemplateConfig.CompletionConfig(
0.2, 0.5, 0, 0, 2000, new ArrayList<>()))
.invokeAsync(text)
.block();
if (summary != null) {
System.out.println("Result: " + summary.getResult());
} else {
System.out.println("Null result");
}
}
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.guice;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
public class GuiceEmbeddingExample {
private final EmbeddingGeneration<String, Double> embeddingGeneration;
public static void main(String[] args) {
Injector injector =
Guice.createInjector(
new SemanticKernelModule()
.withMemory()
.withTextEmbeddingsGenerationService("text-embedding-ada-002"));
GuiceEmbeddingExample example = injector.getInstance(GuiceEmbeddingExample.class);
example.run();
}
@Inject
public GuiceEmbeddingExample(EmbeddingGeneration<String, Double> embeddingGeneration) {
this.embeddingGeneration = embeddingGeneration;
}
public void run() {
List<String> data = new ArrayList<>();
data.add("This is just a test");
embeddingGeneration
.generateEmbeddingsAsync(data)
.block()
.forEach(
doubleEmbedding -> {
doubleEmbedding.getVector().forEach(System.out::println);
});
}
}
@@ -2,6 +2,7 @@
package com.microsoft.semantickernel;
import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration;
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
import com.microsoft.semantickernel.orchestration.SKFunction;
import com.microsoft.semantickernel.textcompletion.TextCompletion;
@@ -14,6 +15,7 @@ public final class KernelConfig {
private static final String DEFAULT_SERVICE_ID = "__SK_DEFAULT";
private final Map<String, Function<Kernel, TextCompletion>> textCompletionServices;
private final Map<String, Function<Kernel, ChatCompletion>> chatCompletionServices;
private final Map<String, Function<Kernel, EmbeddingGeneration<String, Float>>>
textEmbeddingGenerationServices;
@@ -23,10 +25,12 @@ public final class KernelConfig {
Map<String, Function<Kernel, TextCompletion>> textCompletionServices,
Map<String, Function<Kernel, EmbeddingGeneration<String, Float>>>
textEmbeddingGenerationServices,
Map<String, Function<Kernel, ChatCompletion>> chatCompletionServices,
List<SKFunction<?, ?>> skills) {
this.textCompletionServices = new HashMap<>();
this.textCompletionServices.putAll(textCompletionServices);
this.textEmbeddingGenerationServices = new HashMap<>(textEmbeddingGenerationServices);
this.chatCompletionServices = new HashMap<>(chatCompletionServices);
this.skills = new ArrayList<>(skills);
}
@@ -54,6 +58,21 @@ public final class KernelConfig {
return this.textCompletionServices.get(serviceId);
}
public Function<Kernel, ChatCompletion> getChatCompletionServiceOrDefault(
@Nullable String serviceId) {
if (serviceId == null) {
serviceId = DEFAULT_SERVICE_ID;
}
if (!this.chatCompletionServices.containsKey(serviceId)) {
throw new KernelException(
KernelException.ErrorCodes.ServiceNotFound,
"A chat completion service id '" + serviceId + "' doesn't exist");
}
return this.chatCompletionServices.get(serviceId);
}
public static class Builder {
private Map<String, Function<Kernel, TextCompletion>> textCompletionServices =
new HashMap<>();
@@ -62,6 +81,8 @@ public final class KernelConfig {
private Map<String, Function<Kernel, EmbeddingGeneration<String, Float>>>
textEmbeddingGenerationServices = new HashMap<>();
private final Map<String, Function<Kernel, ChatCompletion>> chatCompletionServices =
new HashMap<>();
public Builder addSkill(SKFunction<?, ?> functionDefinition) {
skillBuilders.add(functionDefinition);
@@ -111,10 +132,41 @@ public final class KernelConfig {
return this;
}
/**
* Add to the list a service for chat completion, e.g. OpenAI ChatGPT.
*
* @param serviceId Id used to identify the service
* @param serviceFactory Function used to instantiate the service object
* @return Current object instance
*/
public Builder addChatCompletionService(
@Nullable String serviceId, Function<Kernel, ChatCompletion> serviceFactory) {
if (serviceId != null && serviceId.toUpperCase().equals(DEFAULT_SERVICE_ID)) {
String msg =
"The service id '"
+ serviceId
+ "' is reserved, please use a different name";
throw new KernelException(
KernelException.ErrorCodes.InvalidServiceConfiguration, msg);
}
if (serviceId == null) {
serviceId = DEFAULT_SERVICE_ID;
}
this.chatCompletionServices.put(serviceId, serviceFactory);
if (this.chatCompletionServices.size() == 1) {
this.chatCompletionServices.put(DEFAULT_SERVICE_ID, serviceFactory);
}
return this;
}
public KernelConfig build() {
return new KernelConfig(
Collections.unmodifiableMap(textCompletionServices),
textEmbeddingGenerationServices,
chatCompletionServices,
skillBuilders);
}
}
@@ -0,0 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel;
public class Verify {
public static void notNull(Object object) {
assert object != null;
}
}
@@ -4,6 +4,7 @@ package com.microsoft.semantickernel.ai.embeddings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* Represents a strongly typed vector of numeric data.
@@ -43,7 +44,7 @@ public class Embedding<EmbeddingType extends Number> {
// unsupported.</exception>
// /// <exception cref="ArgumentNullException">A <c>null</c> vector is passed in.</exception>
public Embedding() {
this.vector = new ArrayList<>();
this.vector = Collections.emptyList();
}
/**
@@ -54,6 +55,33 @@ public class Embedding<EmbeddingType extends Number> {
*/
public Embedding(List<EmbeddingType> vector) {
// Verify.NotNull(vector, nameof(vector));
this.vector = Collections.unmodifiableList(vector);
this.vector =
vector != null ? Collections.unmodifiableList(vector) : Collections.emptyList();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Embedding<?> embedding = (Embedding<?>) o;
return vector.equals(embedding.vector);
}
@Override
public int hashCode() {
return vector.hashCode();
}
@Override
public String toString() {
return "Embedding{"
+ "vector="
+ vector.stream()
.limit(3)
.map(String::valueOf)
.collect(Collectors.joining(", ", "[", vector.size() > 3 ? "...]" : "]"))
+ '}';
}
}
@@ -3,6 +3,7 @@ package com.microsoft.semantickernel.builders;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration;
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
import com.microsoft.semantickernel.memory.SemanticTextMemory;
import com.microsoft.semantickernel.orchestration.ContextVariables;
import com.microsoft.semantickernel.orchestration.SKContext;
@@ -43,6 +44,8 @@ public enum BuildersSingleton {
"com.microsoft.semantickernel.templateengine.DefaultPromptTemplateEngine$Builder";
private static final String FALLBACK_SEMANTIC_TEXT_MEMORY_CLASS =
"com.microsoft.semantickernel.memory.DefaultSemanticTextMemory$Builder";
private static final String FALLBACK_CHAT_COMPLETION_BUILDER_CLASS =
"com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatCompletion$Builder";
private final FunctionBuilders functionBuilders;
private final Kernel.InternalBuilder kernelBuilder;
@@ -53,6 +56,7 @@ public enum BuildersSingleton {
private final ContextVariables.Builder variables;
private final SKContext.Builder context;
private final PromptTemplateEngine.Builder promptTemplateEngine;
private final ChatCompletion.Builder chatCompletion;
private final SemanticTextMemory.Builder semanticTextMemoryBuilder;
@@ -100,6 +104,10 @@ public enum BuildersSingleton {
PromptTemplateEngine.Builder.class,
FALLBACK_PROMPT_TEMPLATE_ENGINE_BUILDER_CLASS);
chatCompletion =
ServiceLoadUtil.findServiceLoader(
ChatCompletion.Builder.class, FALLBACK_CHAT_COMPLETION_BUILDER_CLASS);
} catch (Throwable e) {
Logger LOGGER = LoggerFactory.getLogger(BuildersSingleton.class);
LOGGER.error("Failed to discover Semantic Kernel Builders", e);
@@ -164,4 +172,8 @@ public enum BuildersSingleton {
public SemanticTextMemory.Builder getSemanticTextMemoryBuilder() {
return semanticTextMemoryBuilder;
}
public ChatCompletion.Builder getChatCompletion() {
return chatCompletion;
}
}
@@ -4,6 +4,7 @@ package com.microsoft.semantickernel.builders;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.KernelConfig;
import com.microsoft.semantickernel.ai.embeddings.EmbeddingGeneration;
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
import com.microsoft.semantickernel.memory.SemanticTextMemory;
import com.microsoft.semantickernel.orchestration.ContextVariables;
import com.microsoft.semantickernel.orchestration.SKContext;
@@ -60,4 +61,8 @@ public class SKBuilders {
public static SKContext.Builder context() {
return BuildersSingleton.INST.context();
}
public static ChatCompletion.Builder chatCompletion() {
return BuildersSingleton.INST.getChatCompletion();
}
}
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.chatcompletion;
import com.microsoft.openai.OpenAIAsyncClient;
import reactor.core.publisher.Mono;
import javax.annotation.Nullable;
public interface ChatCompletion<ChatHistoryType extends ChatHistory> {
/**
* Generate a new chat message
*
* @param chat Chat history
* @param requestSettings AI request settings
* @return Generated chat message in string format
*/
Mono<String> generateMessageAsync(
ChatHistoryType chat, @Nullable ChatRequestSettings requestSettings);
/**
* Create a new empty chat instance
*
* @param instructions Optional chat instructions for the AI service
* @return Chat object
*/
ChatHistoryType createNewChat(@Nullable String instructions);
interface Builder {
ChatCompletion<? extends ChatHistory> build(OpenAIAsyncClient client, String modelId);
}
}
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.chatcompletion;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/** Provides a history of messages between the User, Assistant and System */
public class ChatHistory {
private final List<Message> messages;
public ChatHistory() {
this.messages = new ArrayList<>();
}
/**
* @return List of messages in the chat
*/
public List<Message> getMessages() {
return Collections.unmodifiableList(messages);
}
/**
* @return The most recent message in chat
*/
public Optional<Message> getLastMessage() {
if (messages.isEmpty()) {
return Optional.empty();
}
return Optional.of(messages.get(messages.size() - 1));
}
public enum AuthorRoles {
Unknown,
System,
User,
Assistant
}
/** Chat message representation */
public static class Message {
private final AuthorRoles authorRoles;
private final String content;
/**
* Create a new instance
*
* @param authorRoles Role of message author
* @param content Message content
*/
public Message(AuthorRoles authorRoles, String content) {
this.authorRoles = authorRoles;
this.content = content;
}
/**
* @return Role of the message author, e.g. user/assistant/system
*/
public AuthorRoles getAuthorRoles() {
return authorRoles;
}
/**
* @return Message content
*/
public String getContent() {
return content;
}
}
/**
* Add a message to the chat history
*
* @param authorRole Role of the message author
* @param content Message content
*/
public void addMessage(AuthorRoles authorRole, String content) {
this.messages.add(new Message(authorRole, content));
}
}
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.chatcompletion;
import com.microsoft.semantickernel.textcompletion.CompletionRequestSettings;
/** Settings for a chat completion request */
public class ChatRequestSettings extends CompletionRequestSettings {}
@@ -140,6 +140,29 @@ public class MemoryRecord extends DataEntryBase {
metadata, embedding != null ? embedding : Embedding.empty(), key, timestamp);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MemoryRecord that = (MemoryRecord) o;
if (!embedding.equals(that.embedding)) return false;
return metadata.equals(that.metadata);
}
@Override
public int hashCode() {
int result = embedding.hashCode();
result = 31 * result + metadata.hashCode();
return result;
}
@Override
public String toString() {
return "MemoryRecord{" + "embedding=" + embedding + ", metadata=" + metadata + '}';
}
/*
/// <summary>
/// Create a memory record from a serialized metadata string.
@@ -93,4 +93,53 @@ public class MemoryRecordMetadata {
this.externalSourceName = externalSourceName;
this.additionalMetadata = additionalMetadata;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MemoryRecordMetadata that = (MemoryRecordMetadata) o;
if (isReference != that.isReference) return false;
if (!externalSourceName.equals(that.externalSourceName)) return false;
if (!id.equals(that.id)) return false;
if (!description.equals(that.description)) return false;
if (!text.equals(that.text)) return false;
return additionalMetadata.equals(that.additionalMetadata);
}
@Override
public int hashCode() {
int result = (isReference ? 1 : 0);
result = 31 * result + externalSourceName.hashCode();
result = 31 * result + id.hashCode();
result = 31 * result + description.hashCode();
result = 31 * result + text.hashCode();
result = 31 * result + additionalMetadata.hashCode();
return result;
}
@Override
public String toString() {
return "MemoryRecordMetadata{"
+ "isReference="
+ isReference
+ ", externalSourceName='"
+ externalSourceName
+ '\''
+ ", id='"
+ id
+ '\''
+ ", description='"
+ description
+ '\''
+ ", text='"
+ text
+ '\''
+ ", additionalMetadata='"
+ additionalMetadata
+ '\''
+ '}';
}
}
@@ -143,7 +143,7 @@ public interface MemoryStore {
*/
Mono<Tuple2<MemoryRecord, ? extends Number>> getNearestMatchAsync(
@Nonnull String collectionName,
@Nonnull Embedding<? super Number> embedding,
@Nonnull Embedding<? extends Number> embedding,
double minRelevanceScore,
boolean withEmbedding);
}
@@ -2,6 +2,7 @@
package com.microsoft.semantickernel.connectors.ai.openai.azuresdk;
import com.microsoft.openai.OpenAIAsyncClient;
import com.microsoft.semantickernel.ai.AIException;
public abstract class ClientBase {
private final String modelId;
@@ -19,4 +20,14 @@ public abstract class ClientBase {
protected OpenAIAsyncClient getClient() {
return client;
}
protected static void validateMaxTokens(int maxTokens) {
if (maxTokens < 1) {
throw new AIException(
AIException.ErrorCodes.InvalidRequest,
"MaxTokens "
+ maxTokens
+ " is not valid, the value must be greater than zero");
}
}
}
@@ -0,0 +1,137 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.connectors.ai.openai.chatcompletion;
import com.azure.ai.openai.models.ChatCompletionsOptions;
import com.azure.ai.openai.models.ChatMessage;
import com.azure.ai.openai.models.ChatRole;
import com.microsoft.openai.OpenAIAsyncClient;
import com.microsoft.semantickernel.Verify;
import com.microsoft.semantickernel.ai.AIException;
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.chatcompletion.ChatRequestSettings;
import com.microsoft.semantickernel.connectors.ai.openai.azuresdk.ClientBase;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/** OpenAI chat completion client. */
public class OpenAIChatCompletion extends ClientBase implements ChatCompletion<OpenAIChatHistory> {
public OpenAIChatCompletion(OpenAIAsyncClient client, String modelId) {
super(client, modelId);
}
public static class Builder implements ChatCompletion.Builder {
public Builder() {}
public ChatCompletion<OpenAIChatHistory> build(OpenAIAsyncClient client, String modelId) {
return new OpenAIChatCompletion(client, modelId);
}
}
/**
* Generate a new chat message
*
* @param chat Chat history
* @param requestSettings AI request settings
* @return The response generated by the request
*/
public Mono<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.InvalidResponseContent,
"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 -> {
return new ChatMessage(toChatRole(it.getAuthorRoles()))
.setContent(it.getContent());
})
.collect(Collectors.toList());
ChatCompletionsOptions options = new ChatCompletionsOptions(messages);
options.setMaxTokens(requestSettings.getMaxTokens());
options.setTemperature(requestSettings.getTemperature());
options.setTopP(requestSettings.getTopP());
options.setFrequencyPenalty(requestSettings.getFrequencyPenalty());
options.setPresencePenalty(requestSettings.getPresencePenalty());
options.setN(1);
return options;
}
private static ChatRole toChatRole(ChatHistory.AuthorRoles authorRoles) {
switch (authorRoles) {
case System:
return ChatRole.SYSTEM;
case User:
return ChatRole.USER;
case Assistant:
return ChatRole.ASSISTANT;
default:
throw new IllegalArgumentException(
"Invalid chat message author: " + authorRoles.name());
}
}
@Override
public OpenAIChatHistory createNewChat(@Nullable String instructions) {
return internalCreateNewChat(instructions);
}
/**
* Create a new empty chat instance
*
* @param instructions Optional chat instructions for the AI service
* @return Chat object
*/
private static OpenAIChatHistory internalCreateNewChat(@Nullable String instructions) {
if (instructions == null) {
instructions = "";
}
return new OpenAIChatHistory(instructions);
}
}
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.connectors.ai.openai.chatcompletion;
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
import javax.annotation.Nullable;
/** OpenAI Chat content */
public class OpenAIChatHistory extends ChatHistory {
/**
* Create a new and empty chat history
*
* @param assistantInstructions Optional instructions for the assistant
*/
public OpenAIChatHistory(@Nullable String assistantInstructions) {
super();
if (assistantInstructions != null && !assistantInstructions.isEmpty()) {
this.addSystemMessage(assistantInstructions);
}
}
/**
* Add a system message to the chat history
*
* @param content Message content
*/
public void addSystemMessage(String content) {
this.addMessage(AuthorRoles.System, content);
}
/**
* Add an assistant message to the chat history
*
* @param content Message content
*/
public void addAssistantMessage(String content) {
this.addMessage(AuthorRoles.Assistant, content);
}
/**
* Add a user message to the chat history
*
* @param content Message content
*/
public void addUserMessage(String content) {
this.addMessage(AuthorRoles.User, content);
}
}
@@ -0,0 +1 @@
com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatCompletion$Builder
@@ -4,12 +4,17 @@ package com.microsoft.semantickernel;
import com.microsoft.semantickernel.ai.AIException;
import com.microsoft.semantickernel.builders.FunctionBuilders;
import com.microsoft.semantickernel.builders.SKBuilders;
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
import com.microsoft.semantickernel.coreskills.SkillImporter;
import com.microsoft.semantickernel.exceptions.NotSupportedException;
import com.microsoft.semantickernel.exceptions.SkillsNotFoundException;
import com.microsoft.semantickernel.memory.MemoryStore;
import com.microsoft.semantickernel.memory.SemanticTextMemory;
import com.microsoft.semantickernel.orchestration.*;
import com.microsoft.semantickernel.orchestration.ContextVariables;
import com.microsoft.semantickernel.orchestration.DefaultCompletionSKFunction;
import com.microsoft.semantickernel.orchestration.RegistrableSkFunction;
import com.microsoft.semantickernel.orchestration.SKContext;
import com.microsoft.semantickernel.orchestration.SKFunction;
import com.microsoft.semantickernel.semanticfunctions.SemanticFunctionConfig;
import com.microsoft.semantickernel.skilldefinition.DefaultSkillCollection;
import com.microsoft.semantickernel.skilldefinition.FunctionNotFound;
@@ -66,6 +71,16 @@ public class DefaultKernel implements Kernel {
"No text completion service available");
}
return (T) factory.apply(this);
} else if (ChatCompletion.class.isAssignableFrom(clazz)) {
Function<Kernel, ChatCompletion> factory =
kernelConfig.getChatCompletionServiceOrDefault(serviceId);
if (factory == null) {
throw new KernelException(
KernelException.ErrorCodes.ServiceNotFound,
"No chat completion service available");
}
return (T) factory.apply(this);
} else {
// TODO correct exception
@@ -1,31 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel;
import com.microsoft.semantickernel.memory.MemoryStore;
import com.microsoft.semantickernel.memory.VolatileMemoryStore;
import com.microsoft.semantickernel.templateengine.DefaultPromptTemplateEngine;
import com.microsoft.semantickernel.templateengine.PromptTemplateEngine;
import javax.annotation.Nullable;
public class DefaultKernelBuilder implements Kernel.InternalBuilder {
@Override
public Kernel build(
KernelConfig kernelConfig,
@Nullable PromptTemplateEngine promptTemplateEngine,
@Nullable MemoryStore memoryStore) {
if (promptTemplateEngine == null) {
promptTemplateEngine = new DefaultPromptTemplateEngine();
}
if (kernelConfig == null) {
throw new IllegalArgumentException();
}
if (memoryStore == null) {
memoryStore = new VolatileMemoryStore();
}
return new DefaultKernel(kernelConfig, promptTemplateEngine, memoryStore);
}
}
@@ -98,8 +98,8 @@ public class VolatileMemoryStore implements MemoryStore {
@Override
public Mono<MemoryRecord> getAsync(
@Nonnull String collectionName, @Nonnull String key, boolean withEmbedding) {
Map<String, MemoryRecord> collection = getCollection(collectionName);
MemoryRecord record = collection.get(key);
Map<String, MemoryRecord> collection = this._store.get(collectionName);
MemoryRecord record = collection != null ? collection.get(key) : null;
if (record != null) {
if (withEmbedding) {
return Mono.just(record);
@@ -143,16 +143,16 @@ public class VolatileMemoryStore implements MemoryStore {
@Override
public Mono<Void> removeAsync(@Nonnull String collectionName, @Nonnull String key) {
Map<String, MemoryRecord> collection = getCollection(collectionName);
collection.remove(key);
Map<String, MemoryRecord> collection = this._store.get(collectionName);
if (collection != null) collection.remove(key);
return Mono.empty();
}
@Override
public Mono<Void> removeBatchAsync(
@Nonnull String collectionName, @Nonnull Collection<String> keys) {
Map<String, MemoryRecord> collection = getCollection(collectionName);
keys.forEach(key -> collection.remove(key));
Map<String, MemoryRecord> collection = this._store.get(collectionName);
keys.forEach(collection::remove);
return Mono.empty();
}
@@ -220,7 +220,7 @@ public class VolatileMemoryStore implements MemoryStore {
@Override
public Mono<Tuple2<MemoryRecord, ? extends Number>> getNearestMatchAsync(
@Nonnull String collectionName,
@Nonnull Embedding<? super Number> embedding,
@Nonnull Embedding<? extends Number> embedding,
double minRelevanceScore,
boolean withEmbedding) {
@@ -0,0 +1 @@
com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion$Builder
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.connectors.ai.openai.chatcompletion;
import static com.microsoft.semantickernel.DefaultKernelTest.mockCompletionOpenAIAsyncClient;
import com.azure.ai.openai.models.ChatCompletions;
import com.microsoft.openai.AzureOpenAIClient;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.KernelConfig;
import com.microsoft.semantickernel.ai.AIException;
import com.microsoft.semantickernel.builders.SKBuilders;
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.syntaxexamples.Example17ChatGPTTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuples;
import java.util.ArrayList;
public class OpenAIChatCompletionTest {
@Test
public void azureOpenAIChatSampleAsync() {
AzureOpenAIClient client =
mockCompletionOpenAIAsyncClient(Tuples.of("Today is", "A-RESULT"));
String message = "Hi, I'm looking for book suggestions";
Example17ChatGPTTest.mockResponse(client, message, "1st response");
String result = getRunExample(client, message);
Assertions.assertEquals("1st response", result);
}
private static String getRunExample(AzureOpenAIClient client, String message) {
KernelConfig kernelConfig =
SKBuilders.kernelConfig()
.addChatCompletionService(
"gpt-3.5-turbo-0301",
kernel ->
SKBuilders.chatCompletion()
.build(client, "gpt-3.5-turbo-0301"))
.build();
Kernel kernel = SKBuilders.kernel().setKernelConfig(kernelConfig).build();
ChatCompletion<OpenAIChatHistory> chatGPT = kernel.getService(null, ChatCompletion.class);
OpenAIChatHistory chatHistory =
chatGPT.createNewChat("You are a librarian, expert about books");
// First user message
chatHistory.addUserMessage(message);
ChatHistory.Message createdMessage = chatHistory.getLastMessage().get();
Assertions.assertEquals(ChatHistory.AuthorRoles.User, createdMessage.getAuthorRoles());
Assertions.assertEquals(message, createdMessage.getContent());
// First bot assistant message
return chatGPT.generateMessageAsync(chatHistory, null).block();
}
private static class runExample {
public final OpenAIChatHistory chatHistory;
public final String reply;
public runExample(OpenAIChatHistory chatHistory, String reply) {
this.chatHistory = chatHistory;
this.reply = reply;
}
}
@Test
public void emptyResponseThrowsError() {
AzureOpenAIClient client =
mockCompletionOpenAIAsyncClient(Tuples.of("Today is", "A-RESULT"));
String message = "Hi, I'm looking for book suggestions";
ChatCompletions completion = Mockito.mock(ChatCompletions.class);
Mockito.when(completion.getChoices()).thenReturn(new ArrayList<>());
Mockito.when(
client.getChatCompletions(
Mockito.any(),
Mockito.argThat(
msg -> {
return msg != null
&& msg.getMessages()
.get(msg.getMessages().size() - 1)
.getContent()
.equals(message);
})))
.thenReturn(Mono.just(completion));
Assertions.assertThrows(AIException.class, () -> getRunExample(client, message));
}
}
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.syntaxexamples;
import static com.microsoft.semantickernel.DefaultKernelTest.mockCompletionOpenAIAsyncClient;
import com.azure.ai.openai.models.ChatChoice;
import com.azure.ai.openai.models.ChatCompletions;
import com.azure.ai.openai.models.ChatMessage;
import com.microsoft.openai.AzureOpenAIClient;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.KernelConfig;
import com.microsoft.semantickernel.builders.SKBuilders;
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatHistory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuples;
import java.util.Arrays;
public class Example17ChatGPTTest {
@Test
public void azureOpenAIChatSampleAsync() {
AzureOpenAIClient client =
mockCompletionOpenAIAsyncClient(Tuples.of("Today is", "A-RESULT"));
String message = "Hi, I'm looking for book suggestions";
String secondMessage =
"I love history and philosophy, I'd like to learn something new about Greece, any"
+ " suggestion?";
mockResponse(client, message, "1st response");
mockResponse(client, secondMessage, "2nd response");
KernelConfig kernelConfig =
SKBuilders.kernelConfig()
.addChatCompletionService(
"gpt-3.5-turbo-0301",
kernel ->
SKBuilders.chatCompletion()
.build(client, "gpt-3.5-turbo-0301"))
.build();
Kernel kernel = SKBuilders.kernel().setKernelConfig(kernelConfig).build();
ChatCompletion<OpenAIChatHistory> chatGPT = kernel.getService(null, ChatCompletion.class);
OpenAIChatHistory chatHistory =
chatGPT.createNewChat("You are a librarian, expert about books");
// First user message
chatHistory.addUserMessage(message);
ChatHistory.Message createdMessage =
chatHistory.getMessages().get(chatHistory.getMessages().size() - 1);
Assertions.assertEquals(ChatHistory.AuthorRoles.User, createdMessage.getAuthorRoles());
Assertions.assertEquals(message, createdMessage.getContent());
// First bot assistant message
String reply = chatGPT.generateMessageAsync(chatHistory, null).block();
Assertions.assertEquals("1st response", reply);
chatHistory.addAssistantMessage(reply);
// Second user message
chatHistory.addUserMessage(secondMessage);
// Second bot assistant message
reply = chatGPT.generateMessageAsync(chatHistory, null).block();
Assertions.assertEquals("2nd response", reply);
}
public static void mockResponse(AzureOpenAIClient client, String message, String response) {
ChatMessage chatMessage = Mockito.mock(ChatMessage.class);
ChatChoice chatChoice = Mockito.mock(ChatChoice.class);
ChatCompletions completion = Mockito.mock(ChatCompletions.class);
Mockito.when(completion.getChoices()).thenReturn(Arrays.asList(chatChoice));
Mockito.when(chatChoice.getMessage()).thenReturn(chatMessage);
Mockito.when(chatMessage.getContent()).thenReturn(response);
Mockito.when(
client.getChatCompletions(
Mockito.any(),
Mockito.argThat(
msg -> {
return msg != null
&& msg.getMessages()
.get(msg.getMessages().size() - 1)
.getContent()
.equals(message);
})))
.thenReturn(Mono.just(completion));
}
}