mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Java: Merge java-development into main (#3361)
### 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 the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄 --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Mark Wallace <[email protected]> Co-authored-by: Teresa Hoang <[email protected]> Co-authored-by: Gil LaHaye <[email protected]> Co-authored-by: Craig Presti <[email protected]> Co-authored-by: adityachallapally <[email protected]> Co-authored-by: Devis Lucato <[email protected]> Co-authored-by: Gina Triolo <[email protected]> Co-authored-by: Adrian Bonar <[email protected]> Co-authored-by: Gudge <[email protected]> Co-authored-by: Shawn Callegari <[email protected]> Co-authored-by: Roger Barreto <[email protected]> Co-authored-by: Stephen Toub <[email protected]> Co-authored-by: Jadyn <[email protected]> Co-authored-by: Lee Miller <[email protected]> Co-authored-by: name <email> Co-authored-by: Milder Hernandez Cagua <[email protected]> Co-authored-by: Milder Hernandez <[email protected]> Co-authored-by: Yoshio Terada <[email protected]> Co-authored-by: Almir Kazazic <[email protected]> Co-authored-by: Alexander Siniouguine <[email protected]> Co-authored-by: Harleen Thind <[email protected]> Co-authored-by: David Barkol <[email protected]> Co-authored-by: Mark Karle <[email protected]> Co-authored-by: poweihuang0817 <[email protected]> Co-authored-by: Po-Wei Huang <[email protected]> Co-authored-by: Ikko Eltociear Ashimine <[email protected]> Co-authored-by: Adrian Bonar (HE/HIM) <[email protected]> Co-authored-by: Tao Chen <[email protected]> Co-authored-by: Vivien Hung <[email protected]> Co-authored-by: Abby Harrison <[email protected]> Co-authored-by: Abby Harrison <[email protected]> Co-authored-by: Devis Lucato <[email protected]> Co-authored-by: fabio fumarola <[email protected]> Co-authored-by: cdMSFT <[email protected]> Co-authored-by: Dmytro Struk <[email protected]> Co-authored-by: Lex Li <[email protected]> Co-authored-by: Damir Dobric <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Aman Sachan <[email protected]> Co-authored-by: Joowon <[email protected]> Co-authored-by: Richard Edgar <[email protected]> Co-authored-by: nacharya1 <[email protected]> Co-authored-by: Kevdome3000 <[email protected]> Co-authored-by: tech <[email protected]> Co-authored-by: Masumi Morishige <[email protected]> Co-authored-by: Alex Chao <[email protected]> Co-authored-by: Alex Chao <[email protected]> Co-authored-by: markwallace-microsoft <[email protected]> Co-authored-by: David Grieve <[email protected]> Co-authored-by: Bruno Borges <[email protected]> Co-authored-by: Martijn Verburg <[email protected]> Co-authored-by: Luigi Montoya <[email protected]> Co-authored-by: joe-braley <[email protected]> Co-authored-by: Luigi96 <[email protected]> Co-authored-by: GitHub <[email protected]> Co-authored-by: Derek Keeler <[email protected]> Co-authored-by: Joe Braley <[email protected]> Co-authored-by: Craig Presti <[email protected]> Co-authored-by: cschadewitz <[email protected]> Co-authored-by: Desmond Howard <[email protected]> Co-authored-by: Jean-Sylvain Boige <[email protected]> Co-authored-by: Dave Arlin <[email protected]> Co-authored-by: Ben Thomas <[email protected]> Co-authored-by: Ben Thomas <[email protected]> Co-authored-by: Sebastian Santoyo <[email protected]> Co-authored-by: sneha <[email protected]> Co-authored-by: Alisha Maddy <[email protected]> Co-authored-by: Alisha Maddy <[email protected]> Co-authored-by: Sneha <[email protected]> Co-authored-by: Guy Bowerman <[email protected]> Co-authored-by: kshitijcode <[email protected]> Co-authored-by: dantelmomsft <[email protected]> Co-authored-by: Vikram Seshadri <[email protected]>
This commit is contained in:
co-authored by
Mark Wallace
Teresa Hoang
Gil LaHaye
Craig Presti
adityachallapally
Devis Lucato
Gina Triolo
Adrian Bonar
Gudge
Shawn Callegari
Roger Barreto
Stephen Toub
Jadyn
Lee Miller
name
Milder Hernandez Cagua
Milder Hernandez
Yoshio Terada
Almir Kazazic
Alexander Siniouguine
Harleen Thind
David Barkol
Mark Karle
poweihuang0817
Po-Wei Huang
Ikko Eltociear Ashimine
Adrian Bonar
Tao Chen
Vivien Hung
Abby Harrison
Abby Harrison
Devis Lucato
fabio fumarola
cdMSFT
Dmytro Struk
Lex Li
Damir Dobric
dependabot[bot]
Aman Sachan
Joowon
Richard Edgar
nacharya1
Kevdome3000
tech
Masumi Morishige
Alex Chao
Alex Chao
markwallace-microsoft
David Grieve
Bruno Borges
Martijn Verburg
Luigi Montoya
joe-braley
Luigi96
GitHub
Derek Keeler
Joe Braley
Craig Presti
cschadewitz
Desmond Howard
Jean-Sylvain Boige
Dave Arlin
Ben Thomas
Ben Thomas
Sebastian Santoyo
sneha
Alisha Maddy
Alisha Maddy
Sneha
Guy Bowerman
kshitijcode
dantelmomsft
Vikram Seshadri
parent
8d6cc3e951
commit
6d574f263b
@@ -6,9 +6,9 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "experimental*", "feature*" ]
|
||||
branches: ["main", "experimental*", "feature*", "*-development"]
|
||||
schedule:
|
||||
- cron: '17 11 * * 2'
|
||||
- cron: "17 11 * * 2"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -22,45 +22,58 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'csharp', 'python' ]
|
||||
language: ["csharp", "python", "java"]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
if: ${{ matrix.language != 'java' }}
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
if: ${{ matrix.language == 'java' }}
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: microsoft
|
||||
cache: maven
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- name: Build Java
|
||||
if: ${{ matrix.language == 'java' }}
|
||||
run: ./mvnw -B -DskipTests -Pcompile-jdk17 clean install --file pom.xml
|
||||
working-directory: java
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
name: Build Java Semantic Kernel
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main", "java-development" ]
|
||||
paths:
|
||||
- 'java/**'
|
||||
pull_request:
|
||||
branches: [ "main", "java-development" ]
|
||||
paths:
|
||||
- 'java/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
paths-filter:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
javaChanges: ${{ steps.filter.outputs.java }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
java:
|
||||
- 'java/**'
|
||||
- '**/java/**'
|
||||
# run only if 'java' files were changed
|
||||
- name: java tests
|
||||
if: steps.filter.outputs.java == 'true'
|
||||
run: echo "Java file"
|
||||
# run only if not 'java' files were changed
|
||||
- name: not java tests
|
||||
if: steps.filter.outputs.java != 'true'
|
||||
run: echo "NOT java file"
|
||||
java-build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: paths-filter
|
||||
if: needs.paths-filter.outputs.javaChanges == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java-versions: [8, 17]
|
||||
|
||||
name: Java CI on JDK${{ matrix.java-versions }}
|
||||
|
||||
steps:
|
||||
- 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: ${{ steps.set-jdk.outputs.JDK_VERSION }}
|
||||
distribution: microsoft
|
||||
cache: maven
|
||||
|
||||
- name: Build with Maven
|
||||
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
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Run Java Integration Tests and Samples
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main", "java-development" ]
|
||||
paths:
|
||||
- 'java/**'
|
||||
pull_request:
|
||||
branches: [ "main", "java-development" ]
|
||||
paths:
|
||||
- 'java/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
paths-filter:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
javaChanges: ${{ steps.filter.outputs.java }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
java:
|
||||
- 'java/**'
|
||||
- '**/java/**'
|
||||
# run only if 'java' files were changed
|
||||
- name: java tests
|
||||
if: steps.filter.outputs.java == 'true'
|
||||
run: echo "Java file"
|
||||
# run only if not 'java' files were changed
|
||||
- name: not java tests
|
||||
if: steps.filter.outputs.java != 'true'
|
||||
run: echo "NOT java file"
|
||||
java-integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: paths-filter
|
||||
if: needs.paths-filter.outputs.javaChanges == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java-versions: [8, 17]
|
||||
|
||||
name: Java Tests on JDK${{ matrix.java-versions }}
|
||||
|
||||
steps:
|
||||
- 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: ${{ steps.set-jdk.outputs.JDK_VERSION }}
|
||||
distribution: microsoft
|
||||
cache: maven
|
||||
|
||||
- name: Build with Maven
|
||||
run: ./mvnw -B -Pbug-check -DskipTests -Pcompile-jdk${{ matrix.java-versions }} clean install --file pom.xml
|
||||
working-directory: java
|
||||
|
||||
# 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 }}
|
||||
|
||||
# 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 }}
|
||||
@@ -0,0 +1,10 @@
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
|
||||
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
# Semantic Kernel for Java Packages
|
||||
|
||||
The Semantic Kernel has the packages below, all are under the groupId `com.microsoft.semantic-kernel`, and can be imported
|
||||
to maven.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
A BOM is provided that can be used to define the versions of all Semantic Kernel packages.
|
||||
|
||||
```xml
|
||||
<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>
|
||||
```
|
||||
|
||||
## Common Packages
|
||||
|
||||
`semantickernel-bom`
|
||||
: A Maven project BOM that can be used to define the versions of all Semantic Kernel packages.
|
||||
|
||||
`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.
|
||||
|
||||
## Connectors
|
||||
|
||||
`semantickernel-connectors-ai-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.
|
||||
|
||||
```xml
|
||||
<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-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>
|
||||
</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>
|
||||
```
|
||||
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
# Semantic Kernel for Java
|
||||
|
||||
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
|
||||
and frameworks.
|
||||
|
||||
## Get started
|
||||
|
||||
To run the LLM prompts and semantic functions in this kernel, make sure you have
|
||||
an [Open AI API Key](https://openai.com/api/)
|
||||
or [Azure Open AI service key](https://learn.microsoft.com/azure/cognitive-services/openai/).
|
||||
|
||||
### Requirements
|
||||
|
||||
To build the Semantic Kernel for Java, you will need:
|
||||
|
||||
- **Required**:
|
||||
- [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/
|
||||
|
||||
2. Build the project with the Maven Wrapper
|
||||
|
||||
cd semantic-kernel/java
|
||||
./mvnw install
|
||||
|
||||
## 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).
|
||||
|
||||
Alternatively, check the `samples` folder for examples of common AI-enabled scenarios implemented with Semantic Kernel for Java.
|
||||
|
||||
## Discord community
|
||||
|
||||
Join the [Microsoft Semantic Kernel Discord community](https://aka.ms/java-sk-discord) to discuss the Semantic Kernel
|
||||
and get help from the community. We have a `#java` channel for Java-specific questions.
|
||||
|
||||
## Contributing
|
||||
|
||||
### Testing locally
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
found under **Resource Management > Deployments** in the Azure Portal.
|
||||
|
||||
For more information, see the Azure OpenAI documentation
|
||||
on [how to get your Azure OpenAI credentials](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/quickstart?pivots=rest-api&tabs=command-line#retrieve-key-and-endpoint).
|
||||
|
||||
To run the unit tests only, run the following command:
|
||||
|
||||
./mvnw package
|
||||
|
||||
To run all tests, including integration tests that require an OpenAI key, run the following command:
|
||||
|
||||
./mvnw verify
|
||||
|
||||
### Submitting a pull request
|
||||
|
||||
Before submitting a pull request, please make sure that you have run the project with the command:
|
||||
|
||||
```shell
|
||||
./mvnw clean package -Pbug-check
|
||||
```
|
||||
|
||||
The bug-check profile will detect some static analysis issues that will prevent merging as well as apply formatting
|
||||
requirements to the code base.
|
||||
|
||||
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
|
||||
project maintainers.
|
||||
|
||||
Make sure your pull request has an objective title and a clear description explaining the problem and solution.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](../LICENSE).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](../CODE_OF_CONDUCT.md).
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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.10-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>
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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.semanticfunctions.PromptTemplateConfig;
|
||||
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")
|
||||
.withCompletionConfig(
|
||||
new PromptTemplateConfig.CompletionConfig(0.2, 0.5, 0, 0, 2000))
|
||||
.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
// 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.semanticfunctions.PromptTemplateConfig;
|
||||
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")
|
||||
.withCompletionConfig(
|
||||
new PromptTemplateConfig.CompletionConfig(0.2, 0.5, 0, 0, 2000))
|
||||
.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss} %-5level %c{1.} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -0,0 +1,133 @@
|
||||
<?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.10-alpha-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>integration-tests</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</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>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j2-impl</artifactId>
|
||||
<scope>test</scope>
|
||||
</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>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>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>mysql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>1.18.3</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>
|
||||
+721
@@ -0,0 +1,721 @@
|
||||
// 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.awt.*;
|
||||
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());
|
||||
}
|
||||
}
|
||||
+720
@@ -0,0 +1,720 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.mysql;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
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.*;
|
||||
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());
|
||||
}
|
||||
}
|
||||
+720
@@ -0,0 +1,720 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.postgresql;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
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.*;
|
||||
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());
|
||||
}
|
||||
}
|
||||
+711
@@ -0,0 +1,711 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.sqlite;
|
||||
|
||||
import com.microsoft.semantickernel.ai.embeddings.Embedding;
|
||||
import com.microsoft.semantickernel.connectors.memory.jdbc.JDBCMemoryStore;
|
||||
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 org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
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
@@ -0,0 +1,102 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// 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.semanticfunctions.PromptTemplateConfig;
|
||||
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")
|
||||
.withCompletionConfig(
|
||||
new PromptTemplateConfig.CompletionConfig(0.7, 0.5, 0, 0, 2000))
|
||||
.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
@@ -0,0 +1,29 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// 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.semanticfunctions.PromptTemplateConfig;
|
||||
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")
|
||||
.withCompletionConfig(
|
||||
new PromptTemplateConfig.CompletionConfig(0.2, 0.5, 0, 0, 2000))
|
||||
.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());
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// 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.semanticfunctions.PromptTemplateConfig;
|
||||
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.""")
|
||||
.withCompletionConfig(
|
||||
new PromptTemplateConfig.CompletionConfig(0, 0, 0, 0, 256))
|
||||
.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
@@ -0,0 +1,37 @@
|
||||
// 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
@@ -0,0 +1,149 @@
|
||||
// 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()));
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss} %-5level %c{1.} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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.10-alpha-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<groupId>com.microsoft.semantic-kernel.test</groupId>
|
||||
<artifactId>api-test</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler-plugin.version>3.11.0</maven.compiler-plugin.version>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>api-usage-example</module>
|
||||
<module>integration-tests</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-bom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<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>
|
||||
<release>${maven.compiler.release}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FindBugsFilter>
|
||||
|
||||
</FindBugsFilter>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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.10-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>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
@@ -0,0 +1,33 @@
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
// 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(createNewChat(text), chatRequestSettings).map(Arrays::asList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> completeStreamAsync(
|
||||
@Nonnull String text, @Nonnull CompletionRequestSettings requestSettings) {
|
||||
ChatRequestSettings chatRequestSettings = new ChatRequestSettings(requestSettings);
|
||||
return generateMessageStream(createNewChat(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);
|
||||
}
|
||||
|
||||
@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
|
||||
* @return Chat object
|
||||
*/
|
||||
private static OpenAIChatHistory internalCreateNewChat(@Nullable String instructions) {
|
||||
if (instructions == null) {
|
||||
instructions = "";
|
||||
}
|
||||
OpenAIChatHistory history = new OpenAIChatHistory("Assistant is a large language model.");
|
||||
history.addUserMessage(instructions);
|
||||
return history;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// 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)
|
||||
.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(), completionsOptions)
|
||||
.flatMapIterable(Completions::getChoices)
|
||||
.mapNotNull(Choice::getText)
|
||||
.collectList();
|
||||
}
|
||||
|
||||
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())
|
||||
.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
@@ -0,0 +1,70 @@
|
||||
// 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
@@ -0,0 +1 @@
|
||||
com.microsoft.semantickernel.connectors.ai.openai.textembeddings.OpenAITextEmbeddingGeneration$Builder
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.microsoft.semantickernel.connectors.ai.openai.chatcompletion.OpenAIChatCompletion$Builder
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.microsoft.semantickernel.connectors.ai.openai.textcompletion.OpenAITextCompletion$Builder
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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.10-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>
|
||||
</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
@@ -0,0 +1,64 @@
|
||||
// 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
@@ -0,0 +1,179 @@
|
||||
// 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
@@ -0,0 +1,510 @@
|
||||
// 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
@@ -0,0 +1,434 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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.10-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
@@ -0,0 +1,10 @@
|
||||
<?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
@@ -0,0 +1,43 @@
|
||||
// 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
@@ -0,0 +1,495 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
// 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.*;
|
||||
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
@@ -0,0 +1,125 @@
|
||||
// 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
@@ -0,0 +1,61 @@
|
||||
// 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
@@ -0,0 +1,25 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.memory.jdbc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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.10-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>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.0.33</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// 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.*;
|
||||
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
@@ -0,0 +1,61 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-parent</artifactId>
|
||||
<version>0.2.10-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
@@ -0,0 +1,10 @@
|
||||
<?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
@@ -0,0 +1,155 @@
|
||||
// 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
@@ -0,0 +1,64 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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.10-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
@@ -0,0 +1,76 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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.10-alpha-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>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
<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.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel;
|
||||
|
||||
import com.microsoft.semantickernel.settings.GPT3Settings;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
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
@@ -0,0 +1,60 @@
|
||||
// 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
@@ -0,0 +1,75 @@
|
||||
// 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
@@ -0,0 +1,31 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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.10-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-planners</artifactId>
|
||||
<name>Semantic Kernel Planners Extension</name>
|
||||
<description>Planner implementation for Semantic Kernel</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<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>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
// 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.semanticfunctions.PromptTemplateConfig;
|
||||
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)
|
||||
.withCompletionConfig(
|
||||
new PromptTemplateConfig.CompletionConfig(0.0, 0.0, 0.0, 0.0, 1024))
|
||||
.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());
|
||||
} else {
|
||||
return new Plan(goal, variables, () -> kernel.getSkills(), 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 + ".";
|
||||
}
|
||||
}
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
// 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.AbstractSkFunction;
|
||||
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.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 AbstractSkFunction<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) {
|
||||
super(
|
||||
new ArrayList<>(),
|
||||
Plan.class.getName(),
|
||||
"",
|
||||
goal,
|
||||
Collections.emptyList(),
|
||||
kernelSkillsSupplier);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Plan(String goal, @Nullable KernelSkillsSupplier kernelSkillsSupplier) {
|
||||
this(goal, SKBuilders.variables().build(), kernelSkillsSupplier);
|
||||
}
|
||||
|
||||
public Plan(
|
||||
SKFunction<?> function,
|
||||
ContextVariables state,
|
||||
List<String> functionOutputs,
|
||||
KernelSkillsSupplier kernelSkillsSupplier) {
|
||||
super(
|
||||
function.describe().getParameters(),
|
||||
function.getSkillName(),
|
||||
function.getName(),
|
||||
function.getDescription(),
|
||||
formReturnParameters(functionOutputs),
|
||||
kernelSkillsSupplier);
|
||||
|
||||
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) {
|
||||
super(
|
||||
function.describe().getParameters(),
|
||||
function.getSkillName(),
|
||||
function.getName(),
|
||||
function.getDescription(),
|
||||
formReturnParameters(functionOutputs),
|
||||
kernelSkillsSupplier);
|
||||
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) {
|
||||
this(function, SKBuilders.variables().build(), functionOutputs, kernelSkillsSupplier);
|
||||
}
|
||||
|
||||
public Plan(SKFunction<?> function, KernelSkillsSupplier kernelSkillsSupplier) {
|
||||
this(function, SKBuilders.variables().build(), new ArrayList<>(), kernelSkillsSupplier);
|
||||
}
|
||||
|
||||
public Plan(
|
||||
String goal,
|
||||
ContextVariables parameters,
|
||||
KernelSkillsSupplier kernelSkillsSupplier,
|
||||
SKFunction<?>... steps) {
|
||||
this(goal, parameters, kernelSkillsSupplier);
|
||||
this.addSteps(steps);
|
||||
}
|
||||
|
||||
public Plan(String goal, KernelSkillsSupplier kernelSkillsSupplier, SKFunction<?>... steps) {
|
||||
this(goal, kernelSkillsSupplier);
|
||||
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()))
|
||||
.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
@@ -0,0 +1,256 @@
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.sequentialplanner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class EmbeddedResource {
|
||||
|
||||
public static String read(String name) {
|
||||
try (InputStream stream = SequentialPlanner.class.getResourceAsStream(name)) {
|
||||
byte[] buffer = new byte[stream.available()];
|
||||
stream.read(buffer);
|
||||
return new String(buffer, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.sequentialplanner;
|
||||
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.orchestration.SKFunction;
|
||||
import com.microsoft.semantickernel.orchestration.WritableContextVariables;
|
||||
import com.microsoft.semantickernel.planner.PlanningException;
|
||||
import com.microsoft.semantickernel.planner.actionplanner.Plan;
|
||||
import com.microsoft.semantickernel.skilldefinition.FunctionView;
|
||||
import com.microsoft.semantickernel.skilldefinition.ReadOnlySkillCollection;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/** Parse sequential plan text into a plan. */
|
||||
public class SequentialPlanParser {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SequentialPlanParser.class);
|
||||
|
||||
// The tag name used in the plan xml for the user's goal/ask.
|
||||
private static final String GoalTag = "goal";
|
||||
|
||||
// The tag name used in the plan xml for the solution.
|
||||
private static final String SolutionTag = "plan";
|
||||
|
||||
// The tag name used in the plan xml for a step that calls a skill function.
|
||||
private static final String FunctionTag = "function.";
|
||||
|
||||
// The attribute tag used in the plan xml for setting the context variable name to set the
|
||||
// output of a function to.
|
||||
private static final String SetContextVariableTag =
|
||||
"setContextVariable".toLowerCase(Locale.ROOT);
|
||||
|
||||
// The attribute tag used in the plan xml for appending the output of a function to the final
|
||||
// result for a plan.
|
||||
private static final String AppendToResultTag = "appendToResult".toLowerCase(Locale.ROOT);
|
||||
|
||||
/**
|
||||
* Convert a plan xml string to a plan
|
||||
*
|
||||
* @param xmlString The plan xml string
|
||||
* @param goal The goal for the plan
|
||||
* @param skills The skills to use
|
||||
* @return The plan
|
||||
* @throws PlanningException
|
||||
*/
|
||||
public static Plan toPlanFromXml(String xmlString, String goal, ReadOnlySkillCollection skills)
|
||||
throws PlanningException {
|
||||
|
||||
try {
|
||||
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
|
||||
Document doc =
|
||||
db.parse(
|
||||
new ByteArrayInputStream(
|
||||
("<xml>" + xmlString + "</xml>")
|
||||
.getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
NodeList solution = doc.getElementsByTagName(SolutionTag);
|
||||
|
||||
Plan plan = new Plan(goal, () -> skills);
|
||||
|
||||
for (int i = 0; i < solution.getLength(); i++) {
|
||||
Node solutionNode = solution.item(i);
|
||||
String parentNodeName = solutionNode.getNodeName();
|
||||
|
||||
for (int j = 0; j < solutionNode.getChildNodes().getLength(); j++) {
|
||||
Node childNode = solutionNode.getChildNodes().item(j);
|
||||
if (childNode.getNodeName().equals("#text")) {
|
||||
if (childNode.getNodeValue() != null
|
||||
&& !childNode.getNodeValue().trim().isEmpty()) {
|
||||
plan.addSteps(new Plan(childNode.getNodeValue().trim(), () -> skills));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (childNode.getNodeName().toLowerCase(Locale.ROOT).startsWith(FunctionTag)) {
|
||||
String[] skillFunctionNameParts =
|
||||
childNode.getNodeName().split(FunctionTag, -1);
|
||||
String skillFunctionName = "";
|
||||
|
||||
if (skillFunctionNameParts.length > 1) {
|
||||
skillFunctionName = skillFunctionNameParts[1];
|
||||
}
|
||||
|
||||
String skillName = getSkillName(skillFunctionName);
|
||||
String functionName = getFunctionName(skillFunctionName);
|
||||
|
||||
if (functionName != null
|
||||
&& !functionName.isEmpty()
|
||||
&& skills.hasFunction(skillName, functionName)) {
|
||||
SKFunction skillFunction =
|
||||
Objects.requireNonNull(skills.getFunctions(skillName))
|
||||
.getFunction(functionName, SKFunction.class);
|
||||
|
||||
WritableContextVariables functionVariables =
|
||||
getFunctionVariables(skillFunction, childNode);
|
||||
List<String> functionOutputs = getFunctionOutputs(childNode);
|
||||
List<String> functionResults = getFunctionResults(childNode);
|
||||
|
||||
Plan planStep =
|
||||
new Plan(
|
||||
skillFunction,
|
||||
functionVariables,
|
||||
SKBuilders.variables().build(),
|
||||
functionOutputs,
|
||||
() -> skills);
|
||||
|
||||
plan.addOutputs(functionResults);
|
||||
plan.addSteps(planStep);
|
||||
} else {
|
||||
LOGGER.trace(
|
||||
"{}: appending function node {}",
|
||||
parentNodeName,
|
||||
skillFunctionName);
|
||||
plan.addSteps(new Plan(childNode.getTextContent(), () -> skills));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
plan.addSteps(new Plan(childNode.getTextContent(), () -> skills));
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
|
||||
} catch (RuntimeException | ParserConfigurationException | IOException | SAXException e) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_PLAN, "Failed to parse plan xml.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static WritableContextVariables getFunctionVariables(
|
||||
SKFunction skillFunction, Node node) {
|
||||
WritableContextVariables functionVariables = SKBuilders.variables().build().writableClone();
|
||||
|
||||
FunctionView description = skillFunction.describe();
|
||||
|
||||
if (description != null) {
|
||||
description
|
||||
.getParameters()
|
||||
.forEach(
|
||||
p -> {
|
||||
functionVariables.setVariable(p.getName(), p.getDefaultValue());
|
||||
});
|
||||
}
|
||||
|
||||
if (node.getAttributes() != null) {
|
||||
for (int k = 0; k < node.getAttributes().getLength(); k++) {
|
||||
Node attr = node.getAttributes().item(k);
|
||||
String nodeName = attr.getNodeName().toLowerCase(Locale.ROOT);
|
||||
if (!nodeName.equals(SetContextVariableTag)
|
||||
&& !nodeName.equals(AppendToResultTag)) {
|
||||
functionVariables.setVariable(attr.getNodeName(), attr.getTextContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
return functionVariables;
|
||||
}
|
||||
|
||||
private static List<String> getFunctionOutputs(Node node) {
|
||||
List<String> functionOutputs = new ArrayList<>();
|
||||
for (int k = 0; k < node.getAttributes().getLength(); k++) {
|
||||
Node attr = node.getAttributes().item(k);
|
||||
String nodeName = attr.getNodeName().toLowerCase(Locale.ROOT);
|
||||
if (nodeName.equals(SetContextVariableTag) || nodeName.equals(AppendToResultTag)) {
|
||||
functionOutputs.add(attr.getTextContent());
|
||||
}
|
||||
}
|
||||
return functionOutputs;
|
||||
}
|
||||
|
||||
private static List<String> getFunctionResults(Node node) {
|
||||
List<String> functionResults = new ArrayList<>();
|
||||
for (int k = 0; k < node.getAttributes().getLength(); k++) {
|
||||
Node attr = node.getAttributes().item(k);
|
||||
String nodeName = attr.getNodeName().toLowerCase(Locale.ROOT);
|
||||
if (nodeName.equals(AppendToResultTag)) {
|
||||
functionResults.add(attr.getTextContent());
|
||||
}
|
||||
}
|
||||
return functionResults;
|
||||
}
|
||||
|
||||
private static String getSkillName(String skillFunctionName) {
|
||||
String[] skillFunctionNameParts = skillFunctionName.split("\\.", -1);
|
||||
return skillFunctionNameParts.length > 0 ? skillFunctionNameParts[0] : "";
|
||||
}
|
||||
|
||||
private static String getFunctionName(String skillFunctionName) {
|
||||
String[] skillFunctionNameParts = skillFunctionName.split("\\.", -1);
|
||||
return skillFunctionNameParts.length > 1 ? skillFunctionNameParts[1] : skillFunctionName;
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// 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.PlanningException;
|
||||
import com.microsoft.semantickernel.planner.actionplanner.Plan;
|
||||
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/** A planner that uses semantic function to create a sequential plan. */
|
||||
public class SequentialPlanner {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SequentialPlanner.class);
|
||||
private static final String StopSequence = "<!--";
|
||||
|
||||
// The name to use when creating semantic functions that are restricted from plan creation
|
||||
private static final String RestrictedSkillName = "SequentialPlanner_Excluded";
|
||||
|
||||
private final SequentialPlannerRequestSettings config;
|
||||
|
||||
// the function flow semantic function, which takes a goal and creates an xml plan that can be
|
||||
// executed
|
||||
private final CompletionSKFunction functionFlowFunction;
|
||||
private final Kernel kernel;
|
||||
|
||||
/** Whether to include function outputs in the function manual. */
|
||||
private final boolean includeFunctionOutputs;
|
||||
|
||||
public SequentialPlanner(
|
||||
Kernel kernel,
|
||||
@Nullable SequentialPlannerRequestSettings config,
|
||||
@Nullable String prompt) {
|
||||
this(kernel, config, prompt, false);
|
||||
}
|
||||
|
||||
public SequentialPlanner(
|
||||
Kernel kernel,
|
||||
@Nullable SequentialPlannerRequestSettings config,
|
||||
@Nullable String prompt,
|
||||
boolean includeFunctionOutputs) {
|
||||
// Verify.NotNull(kernel);
|
||||
|
||||
if (config == null) {
|
||||
config = new SequentialPlannerRequestSettings();
|
||||
}
|
||||
|
||||
this.config = config.addExcludedSkillName(RestrictedSkillName);
|
||||
|
||||
String promptTemplate;
|
||||
if (prompt != null) {
|
||||
promptTemplate = prompt;
|
||||
} else {
|
||||
promptTemplate = EmbeddedResource.read("skprompt.txt");
|
||||
}
|
||||
|
||||
this.functionFlowFunction =
|
||||
SKBuilders.completionFunctions()
|
||||
.withKernel(kernel)
|
||||
.withPromptTemplate(promptTemplate)
|
||||
.withSkillName(RestrictedSkillName)
|
||||
.withDescription(
|
||||
"Given a request or command or goal generate a step by step plan to"
|
||||
+ " fulfill the request using functions. This ability is also"
|
||||
+ " known as decision making and function flow")
|
||||
.withCompletionConfig(
|
||||
new PromptTemplateConfig.CompletionConfig(
|
||||
0.0, 0.0, 0.0, 0.0, this.config.getMaxTokens()))
|
||||
.build();
|
||||
|
||||
this.kernel = kernel;
|
||||
this.includeFunctionOutputs = includeFunctionOutputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a plan for a goal using a default context build on the current kernel.
|
||||
*
|
||||
* @param goal The goal to create a plan for.
|
||||
* @return The plan
|
||||
*/
|
||||
public Mono<Plan> createPlanAsync(String goal) {
|
||||
return createPlanAsync(goal, SKBuilders.context().withKernel(kernel).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a plan for a goal.
|
||||
*
|
||||
* @param goal The goal to create a plan for.
|
||||
* @param context The semantic kernel context
|
||||
* @return The plan
|
||||
*/
|
||||
public Mono<Plan> createPlanAsync(String goal, SKContext context) {
|
||||
if (goal == null || goal.isEmpty()) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_GOAL, "The goal specified is empty");
|
||||
}
|
||||
|
||||
try {
|
||||
return new DefaultSequentialPlannerSKContext(context)
|
||||
.getFunctionsManualAsync(goal, this.config, includeFunctionOutputs)
|
||||
.flatMap(
|
||||
relevantFunctionsManual -> {
|
||||
SKContext updatedContext =
|
||||
context.setVariable(
|
||||
"available_functions",
|
||||
relevantFunctionsManual)
|
||||
.update(goal);
|
||||
|
||||
return functionFlowFunction.invokeAsync(updatedContext, null);
|
||||
})
|
||||
.map(
|
||||
planResult -> {
|
||||
String planResultString = planResult.getResult().trim();
|
||||
|
||||
LOGGER.debug("Plan result: " + planResultString);
|
||||
|
||||
Plan plan =
|
||||
SequentialPlanParser.toPlanFromXml(
|
||||
planResultString, goal, context.getSkills());
|
||||
return plan;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_PLAN,
|
||||
"Plan parsing error, invalid XML",
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.sequentialplanner;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/// <summary>
|
||||
/// Common configuration for planner instances.
|
||||
/// </summary>
|
||||
public class SequentialPlannerRequestSettings {
|
||||
|
||||
/// <summary>
|
||||
/// The minimum relevancy score for a function to be considered
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Depending on the embeddings engine used, the user ask, the step goal
|
||||
/// and the functions available, this value may need to be adjusted.
|
||||
/// For default, this is set to null to exhibit previous behavior.
|
||||
/// </remarks>
|
||||
@Nullable private Float relevancyThreshold = null;
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// The maximum number of relevant functions to include in the plan.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Limits the number of relevant functions as result of semantic
|
||||
/// search included in the plan creation request.
|
||||
/// <see cref="IncludedFunctions"/> will be included
|
||||
/// in the plan regardless of this limit.
|
||||
/// </remarks>
|
||||
*/
|
||||
private int maxRelevantFunctions = 100;
|
||||
|
||||
/// <summary>
|
||||
/// A list of skills to exclude from the plan creation request.
|
||||
/// </summary>
|
||||
private Set<String> excludedSkills = new HashSet<>();
|
||||
/// <summary>
|
||||
/// A list of functions to exclude from the plan creation request.
|
||||
/// </summary>
|
||||
private Set<String> excludedFunctions = new HashSet<>();
|
||||
|
||||
/// <summary>
|
||||
/// A list of functions to include in the plan creation request.
|
||||
/// </summary>
|
||||
private Set<String> includedFunctions = new HashSet<>();
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of tokens to allow in a plan.
|
||||
/// </summary>
|
||||
private int maxTokens = 1024;
|
||||
|
||||
public SequentialPlannerRequestSettings(
|
||||
@Nullable Float relevancyThreshold,
|
||||
int maxRelevantFunctions,
|
||||
Set<String> excludedSkills,
|
||||
Set<String> excludedFunctions,
|
||||
Set<String> includedFunctions,
|
||||
int maxTokens) {
|
||||
this.relevancyThreshold = relevancyThreshold;
|
||||
this.maxRelevantFunctions = maxRelevantFunctions;
|
||||
this.excludedSkills = Collections.unmodifiableSet(excludedSkills);
|
||||
this.excludedFunctions = Collections.unmodifiableSet(excludedFunctions);
|
||||
this.includedFunctions = Collections.unmodifiableSet(includedFunctions);
|
||||
this.maxTokens = maxTokens;
|
||||
}
|
||||
|
||||
public SequentialPlannerRequestSettings() {}
|
||||
|
||||
@Nullable
|
||||
public Float getRelevancyThreshold() {
|
||||
return relevancyThreshold;
|
||||
}
|
||||
|
||||
public int getMaxRelevantFunctions() {
|
||||
return maxRelevantFunctions;
|
||||
}
|
||||
|
||||
public Set<String> getExcludedSkills() {
|
||||
return Collections.unmodifiableSet(excludedSkills);
|
||||
}
|
||||
|
||||
public Set<String> getExcludedFunctions() {
|
||||
return Collections.unmodifiableSet(excludedFunctions);
|
||||
}
|
||||
|
||||
public Set<String> getIncludedFunctions() {
|
||||
return Collections.unmodifiableSet(includedFunctions);
|
||||
}
|
||||
|
||||
public int getMaxTokens() {
|
||||
return maxTokens;
|
||||
}
|
||||
|
||||
public SequentialPlannerRequestSettings addExcludedFunctions(String function) {
|
||||
HashSet<String> ex = new HashSet<>(excludedFunctions);
|
||||
ex.add(function);
|
||||
return new SequentialPlannerRequestSettings(
|
||||
relevancyThreshold,
|
||||
maxRelevantFunctions,
|
||||
excludedSkills,
|
||||
ex,
|
||||
includedFunctions,
|
||||
maxTokens);
|
||||
}
|
||||
|
||||
public SequentialPlannerRequestSettings addExcludedSkillName(String skillName) {
|
||||
HashSet<String> ex = new HashSet<>(excludedSkills);
|
||||
ex.add(skillName);
|
||||
return new SequentialPlannerRequestSettings(
|
||||
relevancyThreshold,
|
||||
maxRelevantFunctions,
|
||||
ex,
|
||||
excludedFunctions,
|
||||
includedFunctions,
|
||||
maxTokens);
|
||||
}
|
||||
}
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.stepwiseplanner;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.Verify;
|
||||
import com.microsoft.semantickernel.orchestration.ContextVariables;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.orchestration.SKFunction;
|
||||
import com.microsoft.semantickernel.planner.PlanningException;
|
||||
import com.microsoft.semantickernel.planner.PlanningException.ErrorCodes;
|
||||
import com.microsoft.semantickernel.planner.actionplanner.Plan;
|
||||
import com.microsoft.semantickernel.semanticfunctions.PromptTemplate;
|
||||
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
|
||||
import com.microsoft.semantickernel.semanticfunctions.SemanticFunctionConfig;
|
||||
import com.microsoft.semantickernel.skilldefinition.FunctionView;
|
||||
import com.microsoft.semantickernel.skilldefinition.ReadOnlyFunctionCollection;
|
||||
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.util.EmbeddedResourceLoader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SynchronousSink;
|
||||
|
||||
/// <summary>
|
||||
/// A planner that creates a Stepwise plan using Mrkl systems.
|
||||
/// </summary>
|
||||
/// <remark>
|
||||
/// An implementation of a Mrkl system as described in https://arxiv.org/pdf/2205.00445.pdf
|
||||
/// </remark>
|
||||
|
||||
/**
|
||||
* A planner that creates a Stepwise plan using Mrkl systems.
|
||||
*
|
||||
* <p>An implementation of a Mrkl system as described in https://arxiv.org/pdf/2205.00445.pdf
|
||||
*/
|
||||
public class DefaultStepwisePlanner implements StepwisePlanner {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultStepwisePlanner.class);
|
||||
|
||||
/** The name to use when creating semantic functions that are restricted from plan creation */
|
||||
private static final String RESTRICTED_SKILL_NAME = "StepwisePlanner_Excluded";
|
||||
|
||||
/** The prefix used for the scratch pad */
|
||||
private static final String SCRATCH_PAD_PREFIX =
|
||||
"This was my previous work (but they haven't seen any of it! They only see what I"
|
||||
+ " return as final answer):";
|
||||
|
||||
/** The Thought tag */
|
||||
private static final String THOUGHT = "[THOUGHT]";
|
||||
|
||||
/** The Observation tag */
|
||||
private static final String OBSERVATION = "[OBSERVATION]";
|
||||
|
||||
/** The Action tag */
|
||||
private static final String Action = "[ACTION]";
|
||||
|
||||
/** The regex for parsing the final answer response */
|
||||
private static final Pattern S_FINAL_ANSWER_REGEX =
|
||||
Pattern.compile(
|
||||
"\\[FINAL ANSWER\\](?<finalanswer>.+)", Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
/** The regex for parsing the thought response */
|
||||
private static final Pattern S_THOUGHT_REGEX =
|
||||
Pattern.compile("(\\[THOUGHT\\])?(?<thought>.+)", Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
private static final Pattern s_thoughtActionRemoveRegex =
|
||||
Pattern.compile("(.*)^\\[ACTION\\].+", Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
private static final Pattern S_ACTION_REGEX =
|
||||
Pattern.compile(
|
||||
"\\[ACTION\\][^{}]*((?:\\{|\\{\\{)(?:[^{}]*\\{[^{}]*\\})*[^{}]*(?:\\}|\\}\\}))$",
|
||||
Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
private final Kernel kernel;
|
||||
private final StepwisePlannerConfig config;
|
||||
private final SKFunction<?> systemStepFunction;
|
||||
private final SKContext context;
|
||||
private final ReadOnlyFunctionCollection nativeFunctions;
|
||||
|
||||
/**
|
||||
* Initialize a new instance of the {@link StepwisePlanner} class.
|
||||
*
|
||||
* @param kernel The semantic kernel instance.
|
||||
* @param config Optional configuration object
|
||||
* @param prompt Optional prompt override
|
||||
* @param promptUserConfig Optional prompt config override
|
||||
*/
|
||||
public DefaultStepwisePlanner(
|
||||
Kernel kernel,
|
||||
@Nullable StepwisePlannerConfig config,
|
||||
@Nullable String prompt,
|
||||
@Nullable PromptTemplateConfig promptUserConfig) {
|
||||
Verify.notNull(kernel);
|
||||
this.kernel = kernel;
|
||||
|
||||
if (config == null) {
|
||||
config = new StepwisePlannerConfig();
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
this.config.addExcludedSkills(RESTRICTED_SKILL_NAME);
|
||||
|
||||
PromptTemplateConfig promptConfig;
|
||||
if (promptUserConfig == null) {
|
||||
String promptConfigString = null;
|
||||
try {
|
||||
promptConfigString =
|
||||
EmbeddedResourceLoader.readFile(
|
||||
"config.json", DefaultStepwisePlanner.class);
|
||||
if (!Verify.isNullOrEmpty(promptConfigString)) {
|
||||
promptConfig =
|
||||
new ObjectMapper()
|
||||
.readValue(promptConfigString, PromptTemplateConfig.class);
|
||||
} else {
|
||||
promptConfig = new PromptTemplateConfig();
|
||||
}
|
||||
} catch (FileNotFoundException | JsonProcessingException e) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_CONFIGURATION,
|
||||
"Could not find or parse config.json",
|
||||
e);
|
||||
}
|
||||
} else {
|
||||
promptConfig = promptUserConfig;
|
||||
}
|
||||
|
||||
if (prompt == null) {
|
||||
try {
|
||||
prompt =
|
||||
EmbeddedResourceLoader.readFile(
|
||||
"skprompt.txt", DefaultStepwisePlanner.class);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new PlanningException(
|
||||
PlanningException.ErrorCodes.INVALID_CONFIGURATION,
|
||||
"Could not find skprompt.txt",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
promptConfig =
|
||||
new PromptTemplateConfig(
|
||||
promptConfig.getSchema(),
|
||||
promptConfig.getDescription(),
|
||||
promptConfig.getType(),
|
||||
new PromptTemplateConfig.CompletionConfigBuilder(
|
||||
promptConfig.getCompletionConfig())
|
||||
.maxTokens(this.config.getMaxTokens())
|
||||
.build(),
|
||||
promptConfig.getInput());
|
||||
|
||||
this.systemStepFunction = this.importStepwiseFunction(this.kernel, prompt, promptConfig);
|
||||
|
||||
this.nativeFunctions = this.kernel.importSkill(this, RESTRICTED_SKILL_NAME);
|
||||
|
||||
this.context = SKBuilders.context().withKernel(kernel).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a plan for the specified goal.
|
||||
*
|
||||
* @param goal The goal to create a plan for.
|
||||
* @return The plan.
|
||||
*/
|
||||
public Plan createPlan(String goal) {
|
||||
if (Verify.isNullOrEmpty(goal)) {
|
||||
throw new PlanningException(ErrorCodes.INVALID_GOAL, "The goal specified is empty");
|
||||
}
|
||||
|
||||
String functionDescriptions = this.getFunctionDescriptions();
|
||||
|
||||
SKFunction<?> planStep = nativeFunctions.getFunction("ExecutePlan", SKFunction.class);
|
||||
|
||||
ContextVariables parameters =
|
||||
SKBuilders.variables()
|
||||
.withVariable("functionDescriptions", functionDescriptions)
|
||||
.withVariable("question", goal)
|
||||
.build();
|
||||
/*
|
||||
planStep.addOutputs(Arrays.asList("agentScratchPad", "stepCount", "skillCount", "stepsTaken"));
|
||||
*/
|
||||
return new Plan(goal, parameters, kernel::getSkills, planStep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a plan.
|
||||
*
|
||||
* @param question The question to answer
|
||||
* @param functionDescriptions List of tool descriptions
|
||||
* @param context The context
|
||||
* @return Result of the plan
|
||||
*/
|
||||
@DefineSKFunction(description = "Execute a plan", name = "ExecutePlan")
|
||||
public Mono<SKContext> executePlanAsync(
|
||||
@SKFunctionParameters(name = "question", description = "The question to answer")
|
||||
String question,
|
||||
@SKFunctionParameters(
|
||||
name = "functionDescriptions",
|
||||
description = "List of tool descriptions")
|
||||
String functionDescriptions,
|
||||
SKContext context) {
|
||||
|
||||
if (!Verify.isNullOrEmpty(question)) {
|
||||
|
||||
return Flux.generate(
|
||||
() -> Mono.just(new ArrayList<>()), loopThroughStepExecutions(context))
|
||||
.concatMap(it -> it)
|
||||
.filter(
|
||||
stepsTaken -> {
|
||||
return !stepsTaken.isEmpty()
|
||||
&& stepsTaken.get(stepsTaken.size() - 1).getFinalAnswer()
|
||||
!= null;
|
||||
})
|
||||
.take(1)
|
||||
.single()
|
||||
.map(
|
||||
stepsTaken -> {
|
||||
return returnFinalAnswer(
|
||||
context, stepsTaken.get(stepsTaken.size() - 1), stepsTaken);
|
||||
});
|
||||
} else {
|
||||
return Mono.just(context.update("Question not found."));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops calling executeNextStep until a final answer is found or max iterations hit.
|
||||
*
|
||||
* @param context The context
|
||||
*/
|
||||
private BiFunction<
|
||||
Mono<ArrayList<SystemStep>>,
|
||||
SynchronousSink<Mono<ArrayList<SystemStep>>>,
|
||||
Mono<ArrayList<SystemStep>>>
|
||||
loopThroughStepExecutions(SKContext context) {
|
||||
|
||||
AtomicInteger stepIndex = new AtomicInteger(0);
|
||||
|
||||
return (stepsTakenFlux, sink) -> {
|
||||
sink.next(stepsTakenFlux);
|
||||
|
||||
return stepsTakenFlux.flatMap(
|
||||
stepsTaken -> {
|
||||
if (stepIndex.get() > this.config.getMaxIterations()) {
|
||||
return Mono.error(
|
||||
new PlanningException(
|
||||
ErrorCodes.PLAN_EXECUTION_PRODUCED_NO_RESULTS,
|
||||
"Max iterations exceeded"));
|
||||
}
|
||||
|
||||
// If we have a final answer, we're done, _SHOULD_ not happen
|
||||
if (stepsTaken.size() > 1
|
||||
&& !Verify.isNullOrEmpty(
|
||||
stepsTaken.get(stepsTaken.size() - 1).getFinalAnswer())) {
|
||||
return Mono.just(stepsTaken);
|
||||
}
|
||||
|
||||
return executeNextStep(context, stepIndex.incrementAndGet(), stepsTaken);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private Mono<ArrayList<SystemStep>> executeNextStep(
|
||||
SKContext context, Integer stepIndex, ArrayList<SystemStep> stepsTaken) {
|
||||
String scratchPad = this.createScratchPad(stepsTaken);
|
||||
|
||||
context.setVariable("agentScratchPad", scratchPad);
|
||||
|
||||
return systemStepFunction
|
||||
.invokeAsync(context)
|
||||
.flatMap(
|
||||
llmResponse -> {
|
||||
String actionText =
|
||||
Objects.requireNonNull(llmResponse.getResult()).trim();
|
||||
LOGGER.trace("Response: " + actionText);
|
||||
|
||||
SystemStep nextStep = this.parseResult(actionText);
|
||||
|
||||
stepsTaken.add(nextStep);
|
||||
|
||||
if (!Verify.isNullOrEmpty(nextStep.getFinalAnswer())) {
|
||||
return Mono.just(stepsTaken);
|
||||
} else {
|
||||
LOGGER.trace("Thought: {}", nextStep.getThought());
|
||||
|
||||
if (!Verify.isNullOrEmpty(nextStep.getAction())) {
|
||||
LOGGER.info(
|
||||
"Action: {}. Iteration: {}.",
|
||||
nextStep.getAction(),
|
||||
stepIndex + 1);
|
||||
try {
|
||||
LOGGER.trace(
|
||||
"Action: {}({}). Iteration: {}.",
|
||||
nextStep.getAction(),
|
||||
new ObjectMapper()
|
||||
.writeValueAsString(
|
||||
nextStep.getActionVariables()),
|
||||
stepIndex + 1);
|
||||
} catch (JsonProcessingException e) {
|
||||
return Mono.error(
|
||||
new PlanningException(
|
||||
ErrorCodes.UNKNOWN_ERROR,
|
||||
"Could not serialize action variables",
|
||||
e));
|
||||
}
|
||||
|
||||
return this.invokeActionAsync(
|
||||
nextStep.getAction(),
|
||||
nextStep.getActionVariables())
|
||||
.flatMap(
|
||||
result -> {
|
||||
if (Verify.isNullOrEmpty(result)) {
|
||||
nextStep.setObservation(
|
||||
"Got no result from action");
|
||||
} else {
|
||||
nextStep.setObservation(result);
|
||||
}
|
||||
|
||||
LOGGER.trace(
|
||||
"Observation: {}",
|
||||
nextStep.getObservation());
|
||||
|
||||
return Mono.just(stepsTaken);
|
||||
});
|
||||
|
||||
} else {
|
||||
LOGGER.info("Action: No action to take");
|
||||
return Mono.just(stepsTaken);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private SKContext returnFinalAnswer(
|
||||
SKContext context, SystemStep nextStep, List<SystemStep> stepsTaken) {
|
||||
LOGGER.trace("Final Answer: {}", nextStep.getFinalAnswer());
|
||||
|
||||
context = context.update(nextStep.getFinalAnswer());
|
||||
String updatedScratchPlan = this.createScratchPad(stepsTaken);
|
||||
context.setVariable("agentScratchPad", updatedScratchPlan);
|
||||
|
||||
// Add additional results to the context
|
||||
try {
|
||||
this.addExecutionStatsToContext(stepsTaken, context);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private SystemStep parseResult(String input) {
|
||||
SystemStep result = new SystemStep();
|
||||
result.setOriginalResponse(input);
|
||||
|
||||
// Extract final answer
|
||||
Matcher finalAnswerMatch = S_FINAL_ANSWER_REGEX.matcher(input);
|
||||
|
||||
if (finalAnswerMatch.find()) {
|
||||
result.setFinalAnswer(finalAnswerMatch.group(1).trim());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Extract thought
|
||||
Matcher thoughtMatch = S_THOUGHT_REGEX.matcher(input);
|
||||
|
||||
if (thoughtMatch.find()) {
|
||||
Matcher actionRemove =
|
||||
s_thoughtActionRemoveRegex.matcher(thoughtMatch.group("thought"));
|
||||
if (actionRemove.find()) {
|
||||
result.setThought(actionRemove.group(1).trim());
|
||||
} else {
|
||||
result.setThought(thoughtMatch.group("thought").trim());
|
||||
}
|
||||
|
||||
} else if (!input.contains(Action)) {
|
||||
result.setThought(input);
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected input format");
|
||||
}
|
||||
|
||||
result.setThought(Objects.requireNonNull(result.getThought()).replace(THOUGHT, "").trim());
|
||||
|
||||
// Extract action
|
||||
Matcher actionMatch = S_ACTION_REGEX.matcher(input);
|
||||
|
||||
if (actionMatch.find()) {
|
||||
String json = actionMatch.group(1).trim().replace("`", "");
|
||||
|
||||
// Seen this in some cases, not sure why
|
||||
if (json.startsWith("{{")) {
|
||||
json = json.replaceAll("^\\{\\{", "{");
|
||||
json = json.replaceAll("\\}\\}$", "}");
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
SystemStep systemStepResults = new ObjectMapper().readValue(json, SystemStep.class);
|
||||
|
||||
if (systemStepResults == null) {
|
||||
result.setObservation("System step parsing error, empty JSON: {json}");
|
||||
} else {
|
||||
result.setAction(systemStepResults.getAction());
|
||||
result.setActionVariables(systemStepResults.getActionVariables());
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
result.setObservation("System step parsing error, invalid JSON: " + json);
|
||||
}
|
||||
}
|
||||
|
||||
if (Verify.isNullOrEmpty(result.getThought()) && Verify.isNullOrEmpty(result.getAction())) {
|
||||
result.setObservation(
|
||||
"System step error, no thought or action found. Please give a valid thought"
|
||||
+ " and/or action.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addExecutionStatsToContext(List<SystemStep> stepsTaken, SKContext context)
|
||||
throws JsonProcessingException {
|
||||
context.setVariable("stepCount", Integer.toString(stepsTaken.size()));
|
||||
context.setVariable("stepsTaken", new ObjectMapper().writeValueAsString(stepsTaken));
|
||||
|
||||
Map<String, Integer> actionCounts = new HashMap<>();
|
||||
|
||||
stepsTaken.stream()
|
||||
.filter(step -> !Verify.isNullOrEmpty(step.getAction()))
|
||||
.forEach(
|
||||
step -> {
|
||||
if (!actionCounts.containsKey(step.getAction())) {
|
||||
actionCounts.put(step.getAction(), 0);
|
||||
}
|
||||
|
||||
actionCounts.put(
|
||||
step.getAction(), actionCounts.get(step.getAction()) + 1);
|
||||
});
|
||||
|
||||
String skillCallListWithCounts =
|
||||
actionCounts.keySet().stream()
|
||||
.map(skill -> skill + "(" + actionCounts.get(skill) + ")")
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
String skillCallCountStr =
|
||||
actionCounts.values().stream().reduce(0, Integer::sum).toString();
|
||||
|
||||
context.setVariable("skillCount", skillCallCountStr + " (" + skillCallListWithCounts + ")");
|
||||
}
|
||||
|
||||
private String createScratchPad(List<SystemStep> stepsTaken) {
|
||||
if (stepsTaken.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> scratchPadLines = new ArrayList<>();
|
||||
|
||||
// Add the original first thought
|
||||
scratchPadLines.add(SCRATCH_PAD_PREFIX);
|
||||
scratchPadLines.add(THOUGHT + " " + stepsTaken.get(0).getThought());
|
||||
|
||||
// Keep track of where to insert the next step
|
||||
int insertPoint = scratchPadLines.size();
|
||||
|
||||
// Keep the most recent steps in the scratch pad.
|
||||
for (int i = stepsTaken.size() - 1; i >= 0; i--) {
|
||||
if (scratchPadLines.size() / 4.0 > (this.config.getMaxTokens() * 0.75)) {
|
||||
LOGGER.debug("Scratchpad is too long, truncating. Skipping " + (i + 1) + " steps.");
|
||||
break;
|
||||
}
|
||||
|
||||
SystemStep s = stepsTaken.get(i);
|
||||
|
||||
if (!Verify.isNullOrEmpty(s.getObservation())) {
|
||||
scratchPadLines.add(insertPoint, OBSERVATION + " " + s.getObservation());
|
||||
}
|
||||
|
||||
if (!Verify.isNullOrEmpty(s.getAction())) {
|
||||
try {
|
||||
scratchPadLines.add(
|
||||
insertPoint,
|
||||
Action
|
||||
+ " {{"
|
||||
+ "\"action\": \""
|
||||
+ s.getAction()
|
||||
+ "\","
|
||||
+ "\"action_variables\": "
|
||||
+ new ObjectMapper().writeValueAsString(s.getActionVariables())
|
||||
+ "}}");
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (i != 0) {
|
||||
scratchPadLines.add(insertPoint, THOUGHT + " " + s.getThought());
|
||||
}
|
||||
}
|
||||
|
||||
String scratchPad = String.join("\n", scratchPadLines).trim();
|
||||
|
||||
if (!Verify.isNullOrWhiteSpace(scratchPad)) {
|
||||
LOGGER.trace("Scratchpad: " + scratchPad);
|
||||
}
|
||||
|
||||
return scratchPad;
|
||||
}
|
||||
|
||||
private Mono<String> invokeActionAsync(String actionName, Map<String, String> actionVariables) {
|
||||
List<SKFunction<?>> availableFunctions = this.getAvailableFunctions();
|
||||
Optional<SKFunction<?>> targetFunction =
|
||||
availableFunctions.stream()
|
||||
.filter(
|
||||
f -> {
|
||||
return f.toFullyQualifiedName().equals(actionName)
|
||||
|| f.toFullyQualifiedName()
|
||||
.equals(
|
||||
ReadOnlySkillCollection.GlobalSkill
|
||||
+ "."
|
||||
+ actionName)
|
||||
|| f.toFullyQualifiedName()
|
||||
.equals("GLOBAL_FUNCTIONS." + actionName);
|
||||
})
|
||||
.findFirst();
|
||||
|
||||
if (!targetFunction.isPresent()) {
|
||||
throw new PlanningException(
|
||||
ErrorCodes.UNKNOWN_ERROR, "The function '" + actionName + "' was not found.");
|
||||
}
|
||||
|
||||
SKFunction<?> function =
|
||||
kernel.getFunction(
|
||||
targetFunction.get().getSkillName(), targetFunction.get().getName());
|
||||
|
||||
SKContext actionContext = this.createActionContext(actionVariables);
|
||||
|
||||
return function.invokeAsync(actionContext)
|
||||
.mapNotNull(
|
||||
result -> {
|
||||
LOGGER.trace(
|
||||
"Invoked {}. Result: {}",
|
||||
targetFunction.get().getName(),
|
||||
result.getResult());
|
||||
return result.getResult();
|
||||
});
|
||||
}
|
||||
|
||||
private SKContext createActionContext(Map<String, String> actionVariables) {
|
||||
SKContext actionContext = SKBuilders.context().withKernel(this.kernel).build();
|
||||
|
||||
if (actionVariables != null) {
|
||||
actionVariables.forEach(actionContext::setVariable);
|
||||
}
|
||||
|
||||
return actionContext;
|
||||
}
|
||||
|
||||
private List<SKFunction<?>> getAvailableFunctions() {
|
||||
return this.context.getSkills().getAllFunctions().getAll().stream()
|
||||
.filter(
|
||||
fun ->
|
||||
!this.config.getExcludedSkills().contains(fun.getSkillName())
|
||||
&& !this.config
|
||||
.getExcludedFunctions()
|
||||
.contains(fun.getName()))
|
||||
.sorted(
|
||||
(a, b) ->
|
||||
Comparator.<SKFunction<?>, String>comparing(
|
||||
SKFunction::getSkillName)
|
||||
.thenComparing(SKFunction::getName)
|
||||
.compare(a, b))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private String getFunctionDescriptions() {
|
||||
List<SKFunction<?>> availableFunctions = this.getAvailableFunctions();
|
||||
|
||||
return availableFunctions.stream()
|
||||
.map(x -> toManualString(Objects.requireNonNull(x.describe())))
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private SKFunction<CompletionRequestSettings> importStepwiseFunction(
|
||||
Kernel kernel, String promptTemplate, PromptTemplateConfig config) {
|
||||
PromptTemplate template =
|
||||
SKBuilders.promptTemplate()
|
||||
.withPromptTemplate(promptTemplate)
|
||||
.withPromptTemplateConfig(config)
|
||||
.withPromptTemplateEngine(kernel.getPromptTemplateEngine())
|
||||
.build();
|
||||
|
||||
SemanticFunctionConfig functionConfig = new SemanticFunctionConfig(config, template);
|
||||
|
||||
return kernel.registerSemanticFunction(
|
||||
RESTRICTED_SKILL_NAME, "StepwiseStep", functionConfig);
|
||||
}
|
||||
|
||||
private static String toManualString(FunctionView function) {
|
||||
String inputs =
|
||||
function.getParameters().stream()
|
||||
.map(
|
||||
parameter -> {
|
||||
String defaultValueString = "";
|
||||
|
||||
if (!Verify.isNullOrEmpty(parameter.getDefaultValue())
|
||||
&& !parameter
|
||||
.getDefaultValue()
|
||||
.equals(
|
||||
SKFunctionParameters
|
||||
.NO_DEFAULT_VALUE)) {
|
||||
defaultValueString =
|
||||
"(default='" + parameter.getDefaultValue() + "')";
|
||||
}
|
||||
|
||||
return " - "
|
||||
+ parameter.getName()
|
||||
+ ": "
|
||||
+ parameter.getDescription()
|
||||
+ " "
|
||||
+ defaultValueString;
|
||||
})
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
String functionDescription = function.getDescription().trim();
|
||||
|
||||
if (Verify.isNullOrEmpty(inputs)) {
|
||||
return toFullyQualifiedName(function) + ": " + functionDescription + "\n";
|
||||
}
|
||||
|
||||
return toFullyQualifiedName(function) + ": " + functionDescription + "\n" + inputs + "\n";
|
||||
}
|
||||
|
||||
private static String toFullyQualifiedName(FunctionView function) {
|
||||
return function.getSkillName() + "." + function.getName();
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.stepwiseplanner;
|
||||
|
||||
import com.microsoft.semantickernel.planner.actionplanner.Plan;
|
||||
|
||||
/** Interface for planner that creates a Stepwise plan using Mrkl systems. */
|
||||
public interface StepwisePlanner {
|
||||
|
||||
/**
|
||||
* Create a plan for a goal.
|
||||
*
|
||||
* @param goal The goal to create a plan for.
|
||||
* @return The plan.
|
||||
*/
|
||||
Plan createPlan(String goal);
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.stepwiseplanner;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for Stepwise planner instances.
|
||||
/// </summary>
|
||||
public class StepwisePlannerConfig {
|
||||
|
||||
/// <summary>
|
||||
/// The minimum relevancy score for a function to be considered
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Depending on the embeddings engine used, the user ask, the step goal
|
||||
/// and the functions available, this value may need to be adjusted.
|
||||
/// For default, this is set to null to exhibit previous behavior.
|
||||
/// </remarks>
|
||||
public double relevancyThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of relevant functions to include in the plan.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Limits the number of relevant functions as result of semantic
|
||||
/// search included in the plan creation request.
|
||||
/// <see cref="IncludedFunctions"/> will be included
|
||||
/// in the plan regardless of this limit.
|
||||
/// </remarks>
|
||||
private int maxRelevantFunctions = 100;
|
||||
|
||||
/// <summary>
|
||||
/// A list of skills to exclude from the plan creation request.
|
||||
/// </summary>
|
||||
private final Set<String> excludedSkills = new HashSet<>();
|
||||
|
||||
/// <summary>
|
||||
/// A list of functions to exclude from the plan creation request.
|
||||
/// </summary>
|
||||
private final Set<String> excludedFunctions = new HashSet<>();
|
||||
|
||||
/// <summary>
|
||||
/// A list of functions to include in the plan creation request.
|
||||
/// </summary>
|
||||
private Set<String> includedFunctions = new HashSet<>();
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of tokens to allow in a plan.
|
||||
/// </summary>
|
||||
private int maxTokens = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of iterations to allow in a plan.
|
||||
/// </summary>
|
||||
private int maxIterations = 100;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum time to wait between iterations in milliseconds.
|
||||
/// </summary>
|
||||
private int minIterationTimeMs = 0;
|
||||
|
||||
public void addExcludedSkills(String restrictedSkillName) {
|
||||
excludedSkills.add(restrictedSkillName);
|
||||
}
|
||||
|
||||
public void addExcludedFunctions(String restrictedFunctionName) {
|
||||
excludedFunctions.add(restrictedFunctionName);
|
||||
}
|
||||
|
||||
public int getMaxTokens() {
|
||||
return maxTokens;
|
||||
}
|
||||
|
||||
public int getMaxIterations() {
|
||||
return maxIterations;
|
||||
}
|
||||
|
||||
public Set<String> getExcludedSkills() {
|
||||
return Collections.unmodifiableSet(excludedSkills);
|
||||
}
|
||||
|
||||
public Set<String> getExcludedFunctions() {
|
||||
return Collections.unmodifiableSet(excludedFunctions);
|
||||
}
|
||||
|
||||
public int getMinIterationTimeMs() {
|
||||
return minIterationTimeMs;
|
||||
}
|
||||
|
||||
public int getMaxRelevantFunctions() {
|
||||
return maxRelevantFunctions;
|
||||
}
|
||||
|
||||
public double getRelevancyThreshold() {
|
||||
return relevancyThreshold;
|
||||
}
|
||||
|
||||
public Set<String> getIncludedFunctions() {
|
||||
return Collections.unmodifiableSet(includedFunctions);
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.planner.stepwiseplanner;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/// <summary>
|
||||
/// A step in a Stepwise plan.
|
||||
/// </summary>
|
||||
public class SystemStep {
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the step number.
|
||||
/// </summary>
|
||||
@Nullable private String thought;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action of the step
|
||||
/// </summary>
|
||||
@Nullable private String action;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the variables for the action
|
||||
/// </summary>
|
||||
@JsonProperty("action_variables")
|
||||
@Nullable
|
||||
private Map<String, String> actionVariables;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output of the action
|
||||
/// </summary>
|
||||
@Nullable private String observation;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output of the system
|
||||
/// </summary>
|
||||
@JsonProperty("final_answer")
|
||||
@Nullable
|
||||
private String finalAnswer;
|
||||
|
||||
/// <summary>
|
||||
/// The raw response from the action
|
||||
/// </summary>
|
||||
@JsonProperty("original_response")
|
||||
@Nullable
|
||||
private String originalResponse;
|
||||
|
||||
public SystemStep() {}
|
||||
|
||||
@JsonCreator
|
||||
public SystemStep(
|
||||
@JsonProperty("thought") @Nullable String thought,
|
||||
@JsonProperty("action") @Nullable String action,
|
||||
@JsonProperty("action_variables") @Nullable Map<String, String> actionVariables,
|
||||
@JsonProperty("observation") @Nullable String observation,
|
||||
@JsonProperty("final_answer") @Nullable String finalAnswer,
|
||||
@JsonProperty("original_response") @Nullable String originalResponse) {
|
||||
this.thought = thought;
|
||||
this.action = action;
|
||||
if (actionVariables == null) {
|
||||
this.actionVariables = null;
|
||||
} else {
|
||||
this.actionVariables = Collections.unmodifiableMap(actionVariables);
|
||||
}
|
||||
this.observation = observation;
|
||||
this.finalAnswer = finalAnswer;
|
||||
this.originalResponse = originalResponse;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getThought() {
|
||||
return thought;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Map<String, String> getActionVariables() {
|
||||
if (actionVariables == null) {
|
||||
return null;
|
||||
}
|
||||
return Collections.unmodifiableMap(actionVariables);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getObservation() {
|
||||
return observation;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getFinalAnswer() {
|
||||
return finalAnswer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getOriginalResponse() {
|
||||
return originalResponse;
|
||||
}
|
||||
|
||||
public void setThought(@Nullable String thought) {
|
||||
this.thought = thought;
|
||||
}
|
||||
|
||||
public void setAction(@Nullable String action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public void setActionVariables(@Nullable Map<String, String> actionVariables) {
|
||||
if (actionVariables == null) {
|
||||
this.actionVariables = null;
|
||||
} else {
|
||||
this.actionVariables = Collections.unmodifiableMap(actionVariables);
|
||||
}
|
||||
}
|
||||
|
||||
public void setFinalAnswer(@Nullable String finalAnswer) {
|
||||
this.finalAnswer = finalAnswer;
|
||||
}
|
||||
|
||||
public void setObservation(@Nullable String observation) {
|
||||
this.observation = observation;
|
||||
}
|
||||
|
||||
public void setOriginalResponse(@Nullable String originalResponse) {
|
||||
this.originalResponse = originalResponse;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
A planner takes a list of functions, a goal, and chooses which function to use.
|
||||
For each function the list includes details about the input parameters.
|
||||
[START OF EXAMPLES]
|
||||
{{this.GoodExamples}}
|
||||
{{this.EdgeCaseExamples}}
|
||||
[END OF EXAMPLES]
|
||||
[REAL SCENARIO STARTS HERE]
|
||||
- List of functions:
|
||||
{{this.ListOfFunctions}}
|
||||
- End list of functions.
|
||||
Goal: {{ $input }}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
Create an XML plan step by step, to satisfy the goal given.
|
||||
To create a plan, follow these steps:
|
||||
0. The plan should be as short as possible.
|
||||
1. From a <goal> create a <plan> as a series of <functions>.
|
||||
2. Before using any function in a plan, check that it is present in the most recent [AVAILABLE FUNCTIONS] list. If it is not, do not use it. Do not assume that any function that was previously defined or used in another plan or in [EXAMPLES] is automatically available or compatible with the current plan.
|
||||
3. Only use functions that are required for the given goal.
|
||||
4. A function has a single 'input' and a single 'output' which are both strings and not objects.
|
||||
5. The 'output' from each function is automatically passed as 'input' to the subsequent <function>.
|
||||
6. 'input' does not need to be specified if it consumes the 'output' of the previous function.
|
||||
7. To save an 'output' from a <function>, to pass into a future <function>, use <function.{FunctionName} ... setContextVariable: "<UNIQUE_VARIABLE_KEY>"/>
|
||||
8. To save an 'output' from a <function>, to return as part of a plan result, use <function.{FunctionName} ... appendToResult: "RESULT__<UNIQUE_RESULT_KEY>"/>
|
||||
9. Append an "END" XML comment at the end of the plan.
|
||||
|
||||
[EXAMPLES]
|
||||
[AVAILABLE FUNCTIONS]
|
||||
|
||||
EmailConnector.LookupContactEmail:
|
||||
description: looks up the a contact and retrieves their email address
|
||||
inputs:
|
||||
- input: the name to look up
|
||||
|
||||
EmailConnector.EmailTo:
|
||||
description: email the input text to a recipient
|
||||
inputs:
|
||||
- input: the text to email
|
||||
- recipient: the recipient's email address. Multiple addresses may be included if separated by ';'.
|
||||
|
||||
LanguageHelpers.TranslateTo:
|
||||
description: translate the input to another language
|
||||
inputs:
|
||||
- input: the text to translate
|
||||
- translate_to_language: the language to translate to
|
||||
|
||||
SummarizeSkill.Summarize:
|
||||
description: summarize input text
|
||||
inputs:
|
||||
- input: the text to summarize
|
||||
|
||||
[END AVAILABLE FUNCTIONS]
|
||||
|
||||
<goal>Summarize the input, then translate to japanese and email it to Martin</goal>
|
||||
<plan>
|
||||
<function.SummarizeSkill.Summarize/>
|
||||
<function.LanguageHelpers.TranslateTo translate_to_language="Japanese" setContextVariable="TRANSLATED_TEXT" />
|
||||
<function.EmailConnector.LookupContactEmail input="Martin" setContextVariable="CONTACT_RESULT" />
|
||||
<function.EmailConnector.EmailTo input="$TRANSLATED_TEXT" recipient="$CONTACT_RESULT"/>
|
||||
</plan><!-- END -->
|
||||
|
||||
[AVAILABLE FUNCTIONS]
|
||||
|
||||
_GLOBAL_FUNCTIONS_.GetEmailAddress:
|
||||
description: Gets email address for given contact
|
||||
inputs:
|
||||
- input: the name to look up
|
||||
|
||||
_GLOBAL_FUNCTIONS_.SendEmail:
|
||||
description: email the input text to a recipient
|
||||
inputs:
|
||||
- input: the text to email
|
||||
- recipient: the recipient's email address. Multiple addresses may be included if separated by ';'.
|
||||
|
||||
AuthorAbility.Summarize:
|
||||
description: summarizes the input text
|
||||
inputs:
|
||||
- input: the text to summarize
|
||||
|
||||
Magician.TranslateTo:
|
||||
description: translate the input to another language
|
||||
inputs:
|
||||
- input: the text to translate
|
||||
- translate_to_language: the language to translate to
|
||||
|
||||
[END AVAILABLE FUNCTIONS]
|
||||
|
||||
<goal>Summarize an input, translate to french, and e-mail to John Doe</goal>
|
||||
<plan>
|
||||
<function.AuthorAbility.Summarize/>
|
||||
<function.Magician.TranslateTo translate_to_language="French" setContextVariable="TRANSLATED_SUMMARY"/>
|
||||
<function._GLOBAL_FUNCTIONS_.GetEmailAddress input="John Doe" setContextVariable="EMAIL_ADDRESS"/>
|
||||
<function._GLOBAL_FUNCTIONS_.SendEmail input="$TRANSLATED_SUMMARY" email_address="$EMAIL_ADDRESS"/>
|
||||
</plan><!-- END -->
|
||||
|
||||
[AVAILABLE FUNCTIONS]
|
||||
|
||||
_GLOBAL_FUNCTIONS_.NovelOutline :
|
||||
description: Outlines the input text as if it were a novel
|
||||
inputs:
|
||||
- input: the title of the novel to outline
|
||||
- chapterCount: the number of chapters to outline
|
||||
|
||||
Emailer.EmailTo:
|
||||
description: email the input text to a recipient
|
||||
inputs:
|
||||
- input: the text to email
|
||||
- recipient: the recipient's email address. Multiple addresses may be included if separated by ';'.
|
||||
|
||||
Everything.Summarize:
|
||||
description: summarize input text
|
||||
inputs:
|
||||
- input: the text to summarize
|
||||
|
||||
[END AVAILABLE FUNCTIONS]
|
||||
|
||||
<goal>Create an outline for a children's book with 3 chapters about a group of kids in a club and then summarize it.</goal>
|
||||
<plan>
|
||||
<function._GLOBAL_FUNCTIONS_.NovelOutline input="A group of kids in a club called 'The Thinking Caps' that solve mysteries and puzzles using their creativity and logic." chapterCount="3" />
|
||||
<function.Everything.Summarize/>
|
||||
</plan><!-- END -->
|
||||
|
||||
[END EXAMPLES]
|
||||
|
||||
[AVAILABLE FUNCTIONS]
|
||||
|
||||
{{$available_functions}}
|
||||
|
||||
[END AVAILABLE FUNCTIONS]
|
||||
|
||||
<goal>{{$input}}</goal>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"description": "Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.",
|
||||
"type": "completion",
|
||||
"completion": {
|
||||
"max_tokens": 1024,
|
||||
"temperature": 0,
|
||||
"top_p": 0,
|
||||
"presence_penalty": 0,
|
||||
"frequency_penalty": 0,
|
||||
"stop_sequences": ["[OBSERVATION]", "\n[THOUGHT]"]
|
||||
},
|
||||
"input": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "question",
|
||||
"description": "The question to answer",
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"name": "agentScratchPad",
|
||||
"description": "The agent's scratch pad",
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"name": "functionDescriptions",
|
||||
"description": "The manual of the agent's functions",
|
||||
"defaultValue": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
[INSTRUCTION]
|
||||
Answer the following questions as accurately as possible using the provided functions.
|
||||
|
||||
[AVAILABLE FUNCTIONS]
|
||||
The function definitions below are in the following format:
|
||||
<functionName>: <description>
|
||||
- <parameterName>: <parameterDescription>
|
||||
- ...
|
||||
|
||||
{{$functionDescriptions}}
|
||||
[END AVAILABLE FUNCTIONS]
|
||||
|
||||
[USAGE INSTRUCTIONS]
|
||||
To use the functions, specify a JSON blob representing an action. The JSON blob should contain an "action" key with the name of the function to use, and an "action_variables" key with a JSON object of string values to use when calling the function.
|
||||
Do not call functions directly; they must be invoked through an action.
|
||||
The "action_variables" value should always include an "input" key, even if the input value is empty. Additional keys in the "action_variables" value should match the defined [PARAMETERS] of the named "action" in [AVAILABLE FUNCTIONS].
|
||||
Dictionary values in "action_variables" must be strings and represent the actual values to be passed to the function.
|
||||
Ensure that the $JSON_BLOB contains only a SINGLE action; do NOT return multiple actions.
|
||||
IMPORTANT: Use only the available functions listed in the [AVAILABLE FUNCTIONS] section. Do not attempt to use any other functions that are not specified.
|
||||
|
||||
Here is an example of a valid $JSON_BLOB:
|
||||
{
|
||||
"action": "FUNCTION.NAME",
|
||||
"action_variables": {"INPUT": "some input", "PARAMETER_NAME": "some value", "PARAMETER_NAME_2": "42"}
|
||||
}
|
||||
[END USAGE INSTRUCTIONS]
|
||||
[END INSTRUCTION]
|
||||
|
||||
[THOUGHT PROCESS]
|
||||
[QUESTION]
|
||||
the input question I must answer
|
||||
[THOUGHT]
|
||||
To solve this problem, I should carefully analyze the given question and identify the necessary steps. Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
|
||||
[ACTION]
|
||||
$JSON_BLOB
|
||||
[OBSERVATION]
|
||||
The result of the action will be provided here.
|
||||
... (These Thought/Action/Observation can repeat until the final answer is reached.)
|
||||
[FINAL ANSWER]
|
||||
Once I have gathered all the necessary observations and performed any required actions, I can provide the final answer in a clear and human-readable format.
|
||||
[END THOUGHT PROCESS]
|
||||
|
||||
Let's break down the problem step by step and think about the best approach. Questions and observations should be followed by a single thought and an optional single action to take.
|
||||
|
||||
Begin!
|
||||
|
||||
[QUESTION]
|
||||
{{$question}}
|
||||
{{$agentScratchPad}}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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.10-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-settings-loader</artifactId>
|
||||
<name>Semantic Kernel Settings Loader</name>
|
||||
<description>Semantic Kernel Settings Loader</description>
|
||||
|
||||
<dependencies>
|
||||
<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>
|
||||
</dependencies>
|
||||
</project>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel;
|
||||
|
||||
import com.azure.core.util.ClientOptions;
|
||||
import com.azure.core.util.Header;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class SemanticKernelHttpSettings {
|
||||
|
||||
public static final String DEFAULT_USER_AGENT = "Semantic-Kernel-Java";
|
||||
|
||||
public static ClientOptions getUserAgent(@Nullable Map<String, String> configuredSettings) {
|
||||
|
||||
String useragent;
|
||||
|
||||
if (configuredSettings == null) {
|
||||
useragent = DEFAULT_USER_AGENT;
|
||||
} else {
|
||||
useragent = configuredSettings.getOrDefault("useragent", DEFAULT_USER_AGENT);
|
||||
}
|
||||
|
||||
return new ClientOptions()
|
||||
.setHeaders(Collections.singletonList(new Header("User-Agent", useragent)));
|
||||
}
|
||||
|
||||
public static ClientOptions getUserAgent() {
|
||||
return getUserAgent(null);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.util;
|
||||
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
|
||||
/**
|
||||
* Settings for an OpenAI client that uses a specific OpenAI provider such as openai.com or Azure
|
||||
* OpenAI.
|
||||
*/
|
||||
public abstract class AbstractOpenAIClientSettings {
|
||||
|
||||
public static final String KEY_SUFFIX = "key";
|
||||
|
||||
/**
|
||||
* Check if the settings are valid
|
||||
*
|
||||
* @return true if the settings are valid
|
||||
*/
|
||||
public abstract boolean assertIsValid() throws ConfigurationException;
|
||||
|
||||
/**
|
||||
* Get the OpenAI client key
|
||||
*
|
||||
* @return OpenAI client key
|
||||
*/
|
||||
public abstract String getKey() throws ConfigurationException;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.util;
|
||||
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Settings for Azure OpenAI client */
|
||||
public class AzureOpenAISettings extends AbstractOpenAIClientSettings {
|
||||
private static final String DEFAULT_SETTINGS_PREFIX = "client.azureopenai";
|
||||
private static final String AZURE_OPEN_AI_ENDPOINT_SUFFIX = "endpoint";
|
||||
private static final String AZURE_OPEN_AI_DEPLOYMENT_NAME_SUFFIX = "deploymentname";
|
||||
|
||||
@Nullable private final String key;
|
||||
|
||||
@Nullable private final String endpoint;
|
||||
|
||||
@Nullable private final String deploymentName;
|
||||
private final String settingsPrefix;
|
||||
|
||||
public AzureOpenAISettings(Map<String, String> settings) {
|
||||
this(DEFAULT_SETTINGS_PREFIX, settings);
|
||||
}
|
||||
|
||||
public AzureOpenAISettings(String settingsPrefix, Map<String, String> settings) {
|
||||
this.settingsPrefix = settingsPrefix;
|
||||
key = settings.get(settingsPrefix + "." + KEY_SUFFIX);
|
||||
endpoint = settings.get(settingsPrefix + "." + AZURE_OPEN_AI_ENDPOINT_SUFFIX);
|
||||
deploymentName = settings.get(settingsPrefix + "." + AZURE_OPEN_AI_DEPLOYMENT_NAME_SUFFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Azure OpenAI endpoint
|
||||
*
|
||||
* @return Azure OpenAI endpoint
|
||||
*/
|
||||
public String getEndpoint() throws ConfigurationException {
|
||||
if (endpoint == null) {
|
||||
throw new ConfigurationException(
|
||||
ConfigurationException.ErrorCodes.VALUE_NOT_FOUND,
|
||||
AZURE_OPEN_AI_ENDPOINT_SUFFIX);
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Azure OpenAI deployment name
|
||||
*
|
||||
* @return Azure OpenAI deployment name
|
||||
*/
|
||||
public String getDeploymentName() throws ConfigurationException {
|
||||
if (deploymentName == null) {
|
||||
throw new ConfigurationException(
|
||||
ConfigurationException.ErrorCodes.VALUE_NOT_FOUND,
|
||||
AZURE_OPEN_AI_DEPLOYMENT_NAME_SUFFIX);
|
||||
}
|
||||
return deploymentName;
|
||||
}
|
||||
|
||||
public String getKey() throws ConfigurationException {
|
||||
if (key == null) {
|
||||
throw new ConfigurationException(
|
||||
ConfigurationException.ErrorCodes.VALUE_NOT_FOUND, KEY_SUFFIX);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean assertIsValid() throws ConfigurationException {
|
||||
if (key == null) {
|
||||
throw new ConfigurationException(
|
||||
ConfigurationException.ErrorCodes.VALUE_NOT_FOUND,
|
||||
settingsPrefix + "." + KEY_SUFFIX);
|
||||
}
|
||||
if (endpoint == null) {
|
||||
throw new ConfigurationException(
|
||||
ConfigurationException.ErrorCodes.VALUE_NOT_FOUND,
|
||||
settingsPrefix + "." + AZURE_OPEN_AI_ENDPOINT_SUFFIX);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.util;
|
||||
|
||||
/** OpenAI client type */
|
||||
public enum ClientType {
|
||||
OPEN_AI,
|
||||
AZURE_OPEN_AI
|
||||
};
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.util;
|
||||
|
||||
import static com.microsoft.semantickernel.exceptions.ConfigurationException.ErrorCodes.NO_VALID_CONFIGURATIONS_FOUND;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.core.credential.AzureKeyCredential;
|
||||
import com.azure.core.credential.KeyCredential;
|
||||
import com.azure.core.util.ClientOptions;
|
||||
import com.microsoft.semantickernel.SemanticKernelHttpSettings;
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import com.microsoft.semantickernel.settings.SettingsMap;
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/**
|
||||
* Builds an OpenAI client instance, see {@link SettingsMap} documentation for details on how the
|
||||
* settings are loaded.
|
||||
*
|
||||
* <p>If clientType is not set via OPENAI_CLIENT_TYPE setting, then this will attempt to first
|
||||
* instantiate an OpenAI client, and if that fails, then it will attempt to instantiate an Azure
|
||||
* OpenAI client.
|
||||
*/
|
||||
public class OpenAIClientProvider {
|
||||
|
||||
private static final Logger LOGGER =
|
||||
org.slf4j.LoggerFactory.getLogger(OpenAIClientProvider.class);
|
||||
|
||||
public static final String OPENAI_CLIENT_TYPE = "OPENAI_CLIENT_TYPE";
|
||||
|
||||
@Nullable private static final OpenAIClientProvider DEFAULT_INST;
|
||||
|
||||
static {
|
||||
// Build a configuration following the default precedence
|
||||
OpenAIClientProvider DEFAULT_INST_TMP;
|
||||
try {
|
||||
DEFAULT_INST_TMP = buildProvider(null, null);
|
||||
} catch (ConfigurationException e) {
|
||||
LOGGER.error("Failed to load default settings", e);
|
||||
DEFAULT_INST_TMP = null;
|
||||
}
|
||||
DEFAULT_INST = DEFAULT_INST_TMP;
|
||||
}
|
||||
|
||||
@Nullable private final ClientType clientType;
|
||||
private final Map<String, String> configuredSettings;
|
||||
private final ClientOptions clientOptions;
|
||||
|
||||
/**
|
||||
* Create a new OpenAI client provider
|
||||
*
|
||||
* @param configuredSettings settings to use to configure the client
|
||||
* @param clientType client type
|
||||
*/
|
||||
public OpenAIClientProvider(
|
||||
Map<String, String> configuredSettings, @Nullable ClientType clientType) {
|
||||
this.configuredSettings = Collections.unmodifiableMap(configuredSettings);
|
||||
this.clientType = clientType;
|
||||
this.clientOptions = SemanticKernelHttpSettings.getUserAgent(configuredSettings);
|
||||
}
|
||||
|
||||
private static OpenAIClientProvider buildProvider(
|
||||
@Nullable List<File> propertyFileLocations, @Nullable ClientType clientType)
|
||||
throws ConfigurationException {
|
||||
Map<String, String> settings = getSettings(propertyFileLocations);
|
||||
|
||||
if (clientType == null) {
|
||||
String clientValue = settings.get(OPENAI_CLIENT_TYPE);
|
||||
if (clientValue != null) {
|
||||
clientType = ClientType.valueOf(clientValue);
|
||||
}
|
||||
}
|
||||
|
||||
return new OpenAIClientProvider(settings, clientType);
|
||||
}
|
||||
|
||||
private static Map<String, String> getSettings(@Nullable List<File> propertyFileLocations)
|
||||
throws ConfigurationException {
|
||||
Map<String, String> settings;
|
||||
|
||||
if (propertyFileLocations != null) {
|
||||
settings = SettingsMap.get(propertyFileLocations);
|
||||
} else {
|
||||
settings = SettingsMap.getDefault();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a client, overrides the default locations that are searched for settings
|
||||
*
|
||||
* @param propertyFileLocations locations to look for settings in
|
||||
* @param clientType client type
|
||||
* @return A client instance
|
||||
* @throws ConfigurationException if the settings are not found
|
||||
*/
|
||||
public static OpenAIAsyncClient get(
|
||||
List<File> propertyFileLocations, @Nullable ClientType clientType)
|
||||
throws ConfigurationException {
|
||||
return buildProvider(propertyFileLocations, clientType).getAsyncClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a client, overrides the default locations that are searched for settings
|
||||
*
|
||||
* @param propertyFileLocations locations to look for settings in
|
||||
* @return A client instance
|
||||
* @throws ConfigurationException if the settings are not found
|
||||
*/
|
||||
public static OpenAIAsyncClient get(List<File> propertyFileLocations)
|
||||
throws ConfigurationException {
|
||||
return get(propertyFileLocations, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a client, looks for settings in the default locations as defined by this class, PLUS the
|
||||
* additional locations provided
|
||||
*
|
||||
* @param propertyFileLocations additional locations to look for settings
|
||||
* @param clientType client type
|
||||
* @return A client instance
|
||||
*/
|
||||
public static OpenAIAsyncClient getWithAdditional(
|
||||
List<File> propertyFileLocations, @Nullable ClientType clientType)
|
||||
throws ConfigurationException {
|
||||
Map<String, String> settings = SettingsMap.getWithAdditional(propertyFileLocations);
|
||||
|
||||
if (clientType == null && settings.containsKey(OPENAI_CLIENT_TYPE)) {
|
||||
clientType = ClientType.valueOf(settings.get(OPENAI_CLIENT_TYPE));
|
||||
}
|
||||
|
||||
return new OpenAIClientProvider(settings, clientType).getAsyncClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a client, looks for settings in the default locations as defined by this class, PLUS the
|
||||
* additional locations provided
|
||||
*
|
||||
* @param propertyFileLocations additional locations to look for settings
|
||||
* @return A client instance
|
||||
*/
|
||||
public static OpenAIAsyncClient getWithAdditional(List<File> propertyFileLocations)
|
||||
throws ConfigurationException {
|
||||
return getWithAdditional(propertyFileLocations, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an OpenAI client instance, see {@link SettingsMap} documentation for details on how
|
||||
* the settings are loaded.
|
||||
*
|
||||
* @return A client instance
|
||||
* @throws ConfigurationException If the settings are not found
|
||||
*/
|
||||
public static OpenAIAsyncClient getClient() throws ConfigurationException {
|
||||
if (DEFAULT_INST == null) {
|
||||
throw new ConfigurationException(NO_VALID_CONFIGURATIONS_FOUND);
|
||||
}
|
||||
return DEFAULT_INST.getAsyncClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an OpenAI client instance, the settings for the client are loaded from the following
|
||||
* sources in the order of precedence (lower number in this list overrides a higher number):
|
||||
*
|
||||
* <ul>
|
||||
* <li>1. Environment variables.
|
||||
* <li>2. System properties.
|
||||
* <li>3. Properties file set via CONF_PROPERTIES environment variable.
|
||||
* <li>4. Properties file: ./conf.properties (Not used if CONF_PROPERTIES was set).
|
||||
* <li>5. Properties file: ~/.sk/conf.properties (Not used if CONF_PROPERTIES was set).
|
||||
* </ul>
|
||||
*
|
||||
* <p>If clientType is not set via OPENAI_CLIENT_TYPE setting, then this will attempt to first
|
||||
* instantiate an OpenAI client, and if that fails, then it will attempt to instantiate an Azure
|
||||
* OpenAI client.
|
||||
*
|
||||
* @return A client instance
|
||||
* @throws ConfigurationException If the settings are not found
|
||||
*/
|
||||
public OpenAIAsyncClient getAsyncClient() throws ConfigurationException {
|
||||
// No client type is specified, search for a valid client type
|
||||
if (clientType == null) {
|
||||
LOGGER.debug("No OpenAI client type specified, searching for a valid client type");
|
||||
try {
|
||||
LOGGER.debug("Trying OpenAI client");
|
||||
OpenAIAsyncClient client = buildOpenAIClient();
|
||||
LOGGER.debug("Successfully instantiated OpenAI client");
|
||||
return client;
|
||||
} catch (ConfigurationException e) {
|
||||
LOGGER.debug("No OpenAI client found");
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.debug("Trying Azure OpenAI client");
|
||||
OpenAIAsyncClient client = buildAzureOpenAIClient();
|
||||
LOGGER.debug("Successfully instantiated Azure OpenAI client");
|
||||
return client;
|
||||
} catch (ConfigurationException e) {
|
||||
LOGGER.debug("No Azure OpenAI client found");
|
||||
throw new ConfigurationException(NO_VALID_CONFIGURATIONS_FOUND);
|
||||
}
|
||||
} else {
|
||||
switch (clientType) {
|
||||
case OPEN_AI:
|
||||
return buildOpenAIClient();
|
||||
case AZURE_OPEN_AI:
|
||||
return buildAzureOpenAIClient();
|
||||
default:
|
||||
throw new ConfigurationException(NO_VALID_CONFIGURATIONS_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OpenAIAsyncClient buildOpenAIClient() throws ConfigurationException {
|
||||
OpenAISettings settings = new OpenAISettings(configuredSettings);
|
||||
try {
|
||||
settings.assertIsValid();
|
||||
} catch (ConfigurationException e) {
|
||||
LOGGER.warn("Settings are not valid for OpenAI client");
|
||||
LOGGER.warn(e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
return new OpenAIClientBuilder()
|
||||
.credential(new KeyCredential(settings.getKey()))
|
||||
.clientOptions(clientOptions)
|
||||
.buildAsyncClient();
|
||||
}
|
||||
|
||||
private OpenAIAsyncClient buildAzureOpenAIClient() throws ConfigurationException {
|
||||
AzureOpenAISettings settings = new AzureOpenAISettings(configuredSettings);
|
||||
|
||||
try {
|
||||
settings.assertIsValid();
|
||||
} catch (ConfigurationException e) {
|
||||
LOGGER.warn("Could not instantiate Azure OpenAI client");
|
||||
LOGGER.warn(e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
OpenAIClientBuilder builder =
|
||||
new OpenAIClientBuilder()
|
||||
.endpoint(settings.getEndpoint())
|
||||
.clientOptions(clientOptions)
|
||||
.credential(new AzureKeyCredential(settings.getKey()));
|
||||
|
||||
return builder.buildAsyncClient();
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.util;
|
||||
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class OpenAISettings extends AbstractOpenAIClientSettings {
|
||||
public static final String OPEN_AI_ORGANIZATION_SUFFIX = "organizationid";
|
||||
private static final String DEFAULT_SETTINGS_PREFIX = "client.openai";
|
||||
|
||||
@Nullable private final String key;
|
||||
@Nullable private final String organizationId;
|
||||
private final String settingsPrefix;
|
||||
|
||||
public OpenAISettings(Map<String, String> settings) {
|
||||
this(DEFAULT_SETTINGS_PREFIX, settings);
|
||||
}
|
||||
|
||||
public OpenAISettings(String settingsPrefix, Map<String, String> settings) {
|
||||
this.settingsPrefix = settingsPrefix;
|
||||
key = settings.get(settingsPrefix + "." + KEY_SUFFIX);
|
||||
organizationId = settings.get(settingsPrefix + "." + OPEN_AI_ORGANIZATION_SUFFIX);
|
||||
}
|
||||
|
||||
public String getKey() throws ConfigurationException {
|
||||
if (key == null) {
|
||||
throw new ConfigurationException(
|
||||
ConfigurationException.ErrorCodes.VALUE_NOT_FOUND, "key");
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getOrganizationId() throws ConfigurationException {
|
||||
if (organizationId == null) {
|
||||
throw new ConfigurationException(
|
||||
ConfigurationException.ErrorCodes.VALUE_NOT_FOUND, "organizationid");
|
||||
}
|
||||
return organizationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean assertIsValid() throws ConfigurationException {
|
||||
if (key == null) {
|
||||
throw new ConfigurationException(
|
||||
ConfigurationException.ErrorCodes.VALUE_NOT_FOUND,
|
||||
settingsPrefix + "." + KEY_SUFFIX);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.settings;
|
||||
|
||||
import static com.microsoft.semantickernel.exceptions.ConfigurationException.ErrorCodes.CONFIGURATION_NOT_FOUND;
|
||||
import static com.microsoft.semantickernel.exceptions.ConfigurationException.ErrorCodes.COULD_NOT_READ_CONFIGURATION;
|
||||
import static com.microsoft.semantickernel.exceptions.ConfigurationException.ErrorCodes.NO_VALID_CONFIGURATIONS_FOUND;
|
||||
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/**
|
||||
* Creates a map of settings to be used in configuration, the settings are loaded from the following
|
||||
* sources in the order of precedence (lower number in this list overrides a higher number):
|
||||
*
|
||||
* <ul>
|
||||
* <li>1. System properties.
|
||||
* <li>2. Environment variables.
|
||||
* <li>3. Properties file set via CONF_PROPERTIES environment variable.
|
||||
* <li>4. Properties file: ./conf.properties (Not used if CONF_PROPERTIES was set).
|
||||
* <li>5. Properties file: ~/.sk/conf.properties (Not used if CONF_PROPERTIES was set).
|
||||
* </ul>
|
||||
*/
|
||||
public class SettingsMap {
|
||||
private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(SettingsMap.class);
|
||||
|
||||
public static final String CONF_PROPERTIES_NAME = "CONF_PROPERTIES";
|
||||
public static final String CONF_PROPERTIES =
|
||||
getEnvOrProperty(CONF_PROPERTIES_NAME, "conf.properties");
|
||||
|
||||
private static final List<File> DEFAULT_PROPERTIES_LOCATIONS =
|
||||
Arrays.asList(
|
||||
new File(new File(System.getProperty("user.home"), ".sk"), "conf.properties"),
|
||||
new File("conf.properties"));
|
||||
@Nullable private static Map<String, String> DEFAULT_INST;
|
||||
|
||||
private static void initSettings() {
|
||||
if (DEFAULT_INST != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the default instance
|
||||
Map<String, String> DEFAULT_INST_TMP;
|
||||
try {
|
||||
DEFAULT_INST_TMP = get(DEFAULT_PROPERTIES_LOCATIONS);
|
||||
} catch (ConfigurationException e) {
|
||||
LOGGER.error("Failed to load settings", e);
|
||||
DEFAULT_INST_TMP = null;
|
||||
}
|
||||
DEFAULT_INST = DEFAULT_INST_TMP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings, looks for settings in the default locations
|
||||
*
|
||||
* @return A map of settings
|
||||
*/
|
||||
public static Map<String, String> getDefault() throws ConfigurationException {
|
||||
initSettings();
|
||||
if (DEFAULT_INST == null) {
|
||||
throw new ConfigurationException(NO_VALID_CONFIGURATIONS_FOUND);
|
||||
}
|
||||
return Collections.unmodifiableMap(DEFAULT_INST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings, looks for settings in the locations plus the additional locations provided
|
||||
*
|
||||
* @param propertyFileLocations additional locations to look for settings
|
||||
* @return A client instance
|
||||
*/
|
||||
public static Map<String, String> get(List<File> propertyFileLocations)
|
||||
throws ConfigurationException {
|
||||
return Collections.unmodifiableMap(loadAllSettings(propertyFileLocations));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings, looks for settings in the default locations plus the additional locations
|
||||
* provided
|
||||
*
|
||||
* @param propertyFileLocations additional locations to look for settings
|
||||
* @return A client instance
|
||||
*/
|
||||
public static Map<String, String> getWithAdditional(List<File> propertyFileLocations)
|
||||
throws ConfigurationException {
|
||||
ArrayList<File> locations = new ArrayList<>(DEFAULT_PROPERTIES_LOCATIONS);
|
||||
locations.addAll(propertyFileLocations);
|
||||
return get(locations);
|
||||
}
|
||||
|
||||
private static Map<String, String> loadAllSettings(List<File> propertyFileLocations)
|
||||
throws ConfigurationException {
|
||||
Properties properties = new Properties();
|
||||
|
||||
if (getEnvOrProperty(CONF_PROPERTIES_NAME, null) != null) {
|
||||
// User has explicitly set CONF_PROPERTIES, so ONLY use that and System properties
|
||||
if (Files.isRegularFile(new File(CONF_PROPERTIES).toPath())) {
|
||||
try (FileInputStream fis = new FileInputStream(CONF_PROPERTIES)) {
|
||||
properties.load(fis);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new ConfigurationException(CONFIGURATION_NOT_FOUND, CONF_PROPERTIES);
|
||||
} catch (IOException e) {
|
||||
throw new ConfigurationException(COULD_NOT_READ_CONFIGURATION, CONF_PROPERTIES);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use default locations
|
||||
propertyFileLocations.forEach(
|
||||
file -> {
|
||||
if (Files.isRegularFile(file.toPath())) {
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
properties.load(fis);
|
||||
LOGGER.info("Added settings from: {}", file);
|
||||
} catch (FileNotFoundException e) {
|
||||
LOGGER.info("No config file found at: {}", file);
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Failed to read config file at: {}", file);
|
||||
}
|
||||
} else {
|
||||
LOGGER.info("Did not find configuration file: {}", file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// lowercase and replace _ with ., to convert environment variables to properties
|
||||
Map<String, String> envVariables =
|
||||
System.getenv().entrySet().stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
key -> key.getKey().toLowerCase().replace("_", "."),
|
||||
Entry::getValue));
|
||||
|
||||
// Overlay environment variables
|
||||
properties.putAll(envVariables);
|
||||
|
||||
// Overlay system properties
|
||||
properties.putAll(System.getProperties());
|
||||
|
||||
return properties.stringPropertyNames().stream()
|
||||
.collect(
|
||||
HashMap::new,
|
||||
(m, k) -> m.put(k, properties.getProperty(k)),
|
||||
HashMap::putAll);
|
||||
}
|
||||
|
||||
private static String getEnvOrProperty(String propertyName, String defaultValue) {
|
||||
String env = System.getenv(propertyName);
|
||||
if (env != null && !env.isEmpty()) {
|
||||
return env;
|
||||
}
|
||||
|
||||
String property = System.getProperty(propertyName);
|
||||
if (property != null && !property.isEmpty()) {
|
||||
return property;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.connectors.ai.openai.util;
|
||||
|
||||
import com.azure.ai.openai.OpenAIAsyncClient;
|
||||
import com.microsoft.semantickernel.exceptions.ConfigurationException;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ConfigLoadingTest {
|
||||
|
||||
@Test
|
||||
public void openAiConfigIsLoaded() throws ConfigurationException, IOException {
|
||||
List<String> entries = Arrays.asList("client.openai.key: 123");
|
||||
|
||||
OpenAIAsyncClient client = runWithConfig(entries);
|
||||
Assertions.assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void azureOpenAiConfigIsLoaded() throws ConfigurationException, IOException {
|
||||
List<String> entries =
|
||||
Arrays.asList(
|
||||
"client.azureopenai.key: 123",
|
||||
"client.azureopenai.endpoint: https://endpoint");
|
||||
|
||||
OpenAIAsyncClient client = runWithConfig(entries);
|
||||
Assertions.assertNotNull(client);
|
||||
}
|
||||
|
||||
private static OpenAIAsyncClient runWithConfig(List<String> entries)
|
||||
throws IOException, ConfigurationException {
|
||||
Path dir = Files.createTempDirectory("testConfig");
|
||||
File props = createConfig(dir, entries);
|
||||
return OpenAIClientProvider.get(Arrays.asList(props));
|
||||
}
|
||||
|
||||
private static File createConfig(Path dir, List<String> entries) throws IOException {
|
||||
File props = Files.createTempFile(dir, "a", ".properties").toFile();
|
||||
|
||||
try (FileWriter fos = new FileWriter(props)) {
|
||||
entries.forEach(
|
||||
e -> {
|
||||
try {
|
||||
fos.write(e + System.lineSeparator());
|
||||
} catch (IOException ioException) {
|
||||
ioException.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
fi
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
# Remove \r in case we run on Windows within Git Bash
|
||||
# and check out the repository with auto CRLF management
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
log() {
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
else
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
fi
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
else
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
exit 1
|
||||
fi
|
||||
if [ $wrapperSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
# shellcheck disable=SC2086 # safe args
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
Vendored
+205
@@ -0,0 +1,205 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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.10-alpha-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>semantickernel-plugin-core</artifactId>
|
||||
<name>Semantic Kernel Core Plugins</name>
|
||||
<description>Core plugins used by the Semantic Kernel</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.semantic-kernel</groupId>
|
||||
<artifactId>semantickernel-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<version>3.5.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.coreskills;
|
||||
|
||||
import com.microsoft.semantickernel.Kernel;
|
||||
import com.microsoft.semantickernel.SKBuilders;
|
||||
import com.microsoft.semantickernel.orchestration.SKContext;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
|
||||
import com.microsoft.semantickernel.text.TextChunker;
|
||||
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/** Semantic skill that enables conversations summarization */
|
||||
public class ConversationSummarySkill {
|
||||
// The max tokens to process in a single semantic function call.
|
||||
private static final int MaxTokens = 1024;
|
||||
|
||||
private final CompletionSKFunction summarizeConversationFunction;
|
||||
private final CompletionSKFunction conversationActionItemsFunction;
|
||||
private final CompletionSKFunction conversationTopicsFunction;
|
||||
private final Kernel kernel;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the ConversationSummarySkill class
|
||||
*
|
||||
* @param kernel Kernel instance
|
||||
*/
|
||||
public ConversationSummarySkill(Kernel kernel) {
|
||||
this.kernel = kernel;
|
||||
this.summarizeConversationFunction =
|
||||
SKBuilders.completionFunctions()
|
||||
.withKernel(kernel)
|
||||
.withPromptTemplate(
|
||||
SemanticFunctionConstants.SummarizeConversationDefinition)
|
||||
.withFunctionName("summarize")
|
||||
.withSkillName("ConversationSummarySkill")
|
||||
.withDescription(
|
||||
"Given a section of a conversation transcript, summarize the part"
|
||||
+ " of the conversation.")
|
||||
.withCompletionConfig(
|
||||
SKBuilders.completionConfig()
|
||||
.maxTokens(MaxTokens)
|
||||
.temperature(0.1)
|
||||
.topP(0.5)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
this.conversationActionItemsFunction =
|
||||
SKBuilders.completionFunctions()
|
||||
.withKernel(kernel)
|
||||
.withPromptTemplate(
|
||||
SemanticFunctionConstants.GetConversationActionItemsDefinition)
|
||||
.withFunctionName("ActionItems")
|
||||
.withSkillName("ConversationSummarySkill")
|
||||
.withDescription(
|
||||
"Given a section of a conversation transcript, identify action"
|
||||
+ " items.")
|
||||
.withCompletionConfig(
|
||||
SKBuilders.completionConfig()
|
||||
.maxTokens(MaxTokens)
|
||||
.temperature(0.1)
|
||||
.topP(0.5)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
this.conversationTopicsFunction =
|
||||
SKBuilders.completionFunctions()
|
||||
.withKernel(kernel)
|
||||
.withPromptTemplate(
|
||||
SemanticFunctionConstants.GetConversationTopicsDefinition)
|
||||
.withFunctionName("Topics")
|
||||
.withSkillName("ConversationSummarySkill")
|
||||
.withDescription(
|
||||
"Analyze a conversation transcript and extract key topics worth"
|
||||
+ " remembering.")
|
||||
.withCompletionConfig(
|
||||
SKBuilders.completionConfig()
|
||||
.maxTokens(MaxTokens)
|
||||
.temperature(0.1)
|
||||
.topP(0.5)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a long conversation transcript, summarize the conversation
|
||||
*
|
||||
* @param input A long conversation transcript
|
||||
* @param context The SKContext for function execution
|
||||
* @return A context containing the summarized conversation
|
||||
*/
|
||||
@DefineSKFunction(
|
||||
description = "Given a long conversation transcript, summarize the conversation.",
|
||||
name = "SummarizeConversation")
|
||||
public Mono<SKContext> summarizeConversationAsync(
|
||||
@SKFunctionInputAttribute(description = "A long conversation transcript.") String input,
|
||||
@Nullable SKContext context) {
|
||||
List<String> lines = TextChunker.splitPlainTextLines(input, MaxTokens);
|
||||
List<String> paragraphs = TextChunker.splitPlainTextParagraphs(lines, MaxTokens);
|
||||
|
||||
if (context == null) {
|
||||
context = SKBuilders.context().withSkills(kernel.getSkills()).build();
|
||||
}
|
||||
|
||||
SKContext completionContext = context.copy();
|
||||
|
||||
return this.summarizeConversationFunction.aggregatePartitionedResultsAsync(
|
||||
paragraphs, completionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a long conversation transcript, identify action items
|
||||
*
|
||||
* @param input A long conversation transcript
|
||||
* @param context The SKContext for function execution
|
||||
* @return A context containing the action items
|
||||
*/
|
||||
@DefineSKFunction(
|
||||
description = "Given a long conversation transcript, identify action items.",
|
||||
name = "GetConversationActionItems")
|
||||
public Mono<SKContext> getConversationActionItemsAsync(
|
||||
@SKFunctionInputAttribute(description = "A long conversation transcript.") String input,
|
||||
SKContext context) {
|
||||
List<String> lines = TextChunker.splitPlainTextLines(input, MaxTokens);
|
||||
List<String> paragraphs = TextChunker.splitPlainTextParagraphs(lines, MaxTokens);
|
||||
|
||||
SKContext completionContext = context.copy();
|
||||
|
||||
return this.conversationActionItemsFunction.aggregatePartitionedResultsAsync(
|
||||
paragraphs, completionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a long conversation transcript, identify topics
|
||||
*
|
||||
* @param input A long conversation transcript
|
||||
* @param context The SKContext for function execution
|
||||
* @return A context containing the topics
|
||||
*/
|
||||
@DefineSKFunction(
|
||||
description =
|
||||
"Given a long conversation transcript, identify topics worth remembering.",
|
||||
name = "GetConversationTopics")
|
||||
public Mono<SKContext> getConversationTopicsAsync(
|
||||
@SKFunctionInputAttribute(description = "A long conversation transcript.") String input,
|
||||
SKContext context) {
|
||||
List<String> lines = TextChunker.splitPlainTextLines(input, MaxTokens);
|
||||
List<String> paragraphs = TextChunker.splitPlainTextParagraphs(lines, MaxTokens);
|
||||
|
||||
SKContext completionContext = context.copy();
|
||||
|
||||
return this.conversationTopicsFunction.aggregatePartitionedResultsAsync(
|
||||
paragraphs, completionContext);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.coreskills;
|
||||
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Read and write from a file.
|
||||
*
|
||||
* <p>Usage: kernel.ImportSkill("file", new FileIOSkill());
|
||||
*
|
||||
* <p>Examples:
|
||||
*
|
||||
* <pre>{@code
|
||||
* file.readAsync($path) => "hello world"
|
||||
* file.writeAsync()
|
||||
* }</pre>
|
||||
*/
|
||||
public class FileIOSkill {
|
||||
|
||||
/**
|
||||
* Read a file.
|
||||
*
|
||||
* <p>Example:
|
||||
*
|
||||
* <pre>{@code
|
||||
* file.readAsync($path) => "hello world"
|
||||
* }</pre>
|
||||
*
|
||||
* @param path Source file.
|
||||
* @return File content.
|
||||
*/
|
||||
@DefineSKFunction(description = "Read a file", name = "readAsync")
|
||||
public String readFileAsync(
|
||||
@SKFunctionInputAttribute(description = "Source file") String path,
|
||||
@SKFunctionParameters(
|
||||
name = "charset",
|
||||
description = "Character set to use to read the file",
|
||||
defaultValue = "UTF-8")
|
||||
String charset)
|
||||
throws IOException {
|
||||
Path filePath = Paths.get(path);
|
||||
byte[] fileBytes;
|
||||
fileBytes = Files.readAllBytes(filePath);
|
||||
String fileContents = new String(fileBytes, Charset.forName(charset));
|
||||
return fileContents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file.
|
||||
*
|
||||
* <p>Example:
|
||||
*
|
||||
* <pre>{@code
|
||||
* file.writeAsync()
|
||||
* }</pre>
|
||||
*
|
||||
* @param path The destination file.
|
||||
* @param content Contains the 'content' to write
|
||||
*/
|
||||
@DefineSKFunction(description = "Write a file", name = "writeAsync")
|
||||
public void writeFileAsync(
|
||||
@SKFunctionInputAttribute(description = "Destination file") String path,
|
||||
@SKFunctionParameters(
|
||||
name = "content",
|
||||
description = "File content",
|
||||
defaultValue = "",
|
||||
type = String.class)
|
||||
String content,
|
||||
@SKFunctionParameters(
|
||||
name = "charset",
|
||||
description = "Character set to use to read the file",
|
||||
defaultValue = "UTF-8")
|
||||
String charset)
|
||||
throws IOException {
|
||||
Path filePath = Paths.get(path);
|
||||
Files.write(filePath, content.getBytes(charset));
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.coreskills;
|
||||
|
||||
import com.azure.core.http.HttpClient;
|
||||
import com.azure.core.http.HttpMethod;
|
||||
import com.azure.core.http.HttpRequest;
|
||||
import com.azure.core.http.HttpResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* A skill that provides HTTP functionality.
|
||||
*
|
||||
* <p>Usage:
|
||||
*
|
||||
* <p>Usage: kernel.importSkill(new HttpSkill(), "http");
|
||||
*
|
||||
* <p>Examples:
|
||||
*
|
||||
* <p>{{http.getAsync $url}}
|
||||
*
|
||||
* <p>{{http.postAsync $url}}
|
||||
*
|
||||
* <p>{{http.putAsync $url}}
|
||||
*
|
||||
* <p>{{http.deleteAsync $url}}
|
||||
*/
|
||||
public class HttpSkill {
|
||||
|
||||
private final HttpClient httpClient;
|
||||
|
||||
public HttpSkill() {
|
||||
this.httpClient = HttpClient.createDefault();
|
||||
}
|
||||
|
||||
public HttpSkill(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP GET request to the specified URI and returns body as a string.
|
||||
*
|
||||
* @param url The URI to send the request to.
|
||||
* @return The response body as a string.
|
||||
*/
|
||||
public Mono<String> getAsync(String url) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return Mono.error(new IllegalArgumentException("url cannot be `null` or empty"));
|
||||
}
|
||||
HttpRequest request = new HttpRequest(HttpMethod.GET, url);
|
||||
return httpClient.send(request).flatMap(response -> response.getBodyAsString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP POST request to the specified URI and returns body as a string.
|
||||
*
|
||||
* @param url The URI to send the request to.
|
||||
* @param body The response body as a string.
|
||||
* @return
|
||||
*/
|
||||
public Mono<String> postAsync(String url, String body) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return Mono.error(new IllegalArgumentException("url cannot be `null` or empty"));
|
||||
}
|
||||
HttpRequest request = new HttpRequest(HttpMethod.POST, url);
|
||||
request.setBody(body);
|
||||
return httpClient.send(request).flatMap(response -> response.getBodyAsString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP PUT request to the specified URI and returns body as a string.
|
||||
*
|
||||
* @param url The URI to send the request to.
|
||||
* @param body The response body as a string.
|
||||
* @return
|
||||
*/
|
||||
public Mono<String> putAsync(String url, String body) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return Mono.error(new IllegalArgumentException("url cannot be `null` or empty"));
|
||||
}
|
||||
HttpRequest request = new HttpRequest(HttpMethod.PUT, url);
|
||||
request.setBody(body);
|
||||
return httpClient.send(request).flatMap(response -> response.getBodyAsString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP DELETE request to the specified URI and returns body as a string.
|
||||
*
|
||||
* @param url The URI to send the request to.
|
||||
* @return The response body as a string.
|
||||
*/
|
||||
public Mono<HttpResponse> deleteAsync(String url) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return Mono.error(new IllegalArgumentException("url cannot be `null` or empty"));
|
||||
}
|
||||
HttpRequest request = new HttpRequest(HttpMethod.DELETE, url);
|
||||
return httpClient.send(request).flatMap(response -> Mono.just(response));
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
package com.microsoft.semantickernel.coreskills;
|
||||
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
|
||||
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* MathSkill provides a set of functions to make Math calculations.
|
||||
*
|
||||
* <ul>
|
||||
* <li><code>input</code> is the first number in the operation.
|
||||
* <li><code>amount</code> is the second number in the operation.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Usage:
|
||||
*
|
||||
* <blockquote>
|
||||
*
|
||||
* <pre>
|
||||
* kernel.ImportSkill("math", new MathSkill());
|
||||
*
|
||||
* {{math.add}} => Returns the sum of input and amount
|
||||
* {{math.subtract}} => Returns the differentce of input from amount
|
||||
* </pre>
|
||||
*
|
||||
* </blockquote>
|
||||
*
|
||||
* <p>This skill uses BigDecimal to perform the number conversion and the calculations.
|
||||
*/
|
||||
public class MathSkill {
|
||||
|
||||
/**
|
||||
* Returns the addition result of input and amount values provided.
|
||||
*
|
||||
* <p>This skill attempts to convert the numbers to BigDecimal and then add them.
|
||||
*
|
||||
* <p>If a conversion error occurs, the skill will throw an exception.
|
||||
*
|
||||
* @param input Initial value as string to add the specified amount
|
||||
* @param amount The amount to be added to input
|
||||
* @return The resulting sum as a String.
|
||||
*/
|
||||
@DefineSKFunction(description = "Adds amount to a value.", name = "add")
|
||||
public String add(
|
||||
@SKFunctionInputAttribute(description = "The value to add to.") String input,
|
||||
@SKFunctionParameters(name = "amount", description = "The amount to be added to value.")
|
||||
String amount) {
|
||||
|
||||
BigDecimal bValue = new BigDecimal(input);
|
||||
|
||||
return bValue.add(new BigDecimal(amount)).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subtraction result of input and amount values provided.
|
||||
*
|
||||
* <p>This skill attempts to convert the numbers to BigDecimal and then subtract them.
|
||||
*
|
||||
* <p>If a conversion error occurs, the skill will throw an exception.
|
||||
*
|
||||
* @param input Initial value as string to subtract the specified amount
|
||||
* @param amount The amount to be subtracted from input
|
||||
* @return The resulting difference as a String.
|
||||
*/
|
||||
@DefineSKFunction(description = "Subtracts amount from value.", name = "Subtract")
|
||||
public String subtract(
|
||||
@SKFunctionInputAttribute(description = "The value to subtract from.") String input,
|
||||
@SKFunctionParameters(
|
||||
name = "amount",
|
||||
description = "The amount to be subtracted from value.")
|
||||
String amount) {
|
||||
|
||||
BigDecimal bValue = new BigDecimal(input);
|
||||
|
||||
return bValue.subtract(new BigDecimal(amount)).toString();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user