Merge remote-tracking branch 'origin/main' into experimental-java

This commit is contained in:
Shawn Callegari
2023-07-10 09:05:17 -07:00
181 changed files with 3929 additions and 5305 deletions
+31 -2
View File
@@ -5,29 +5,57 @@
version: 2
updates:
# Maintain dependencies for nuget
- package-ecosystem: "nuget"
directory: "samples/apps/copilot-chat-app/webapi"
schedule:
interval: "weekly"
day: "monday"
labels:
- "copilot chat"
- "dependencies"
# Maintain dependencies for nuget
- package-ecosystem: "nuget"
directory: "dotnet/"
schedule:
interval: "weekly"
day: "monday"
ignore:
# For all System.* and Microsoft.Extensions/Bcl.* packages, ignore all major version updates
- dependency-name: "System.*"
update-types: ["version-update:semver-major"]
- dependency-name: "Microsoft.Extensions.*"
update-types: ["version-update:semver-major"]
- dependency-name: "Microsoft.Bcl.*"
update-types: ["version-update:semver-major"]
labels:
- ".NET"
- "dependencies"
# Maintain dependencies for nuget
- package-ecosystem: "nuget"
directory: "samples/"
directory: "samples/dotnet"
schedule:
interval: "weekly"
day: "monday"
# Maintain dependencies for npm
- package-ecosystem: "npm"
directory: "samples/apps"
schedule:
interval: "weekly"
day: "monday"
# Maintain dependencies for pip
- package-ecosystem: "pip"
directory: "python/"
schedule:
interval: "weekly"
day: "monday"
labels:
- "python"
- "dependencies"
# Maintain dependencies for github-actions
- package-ecosystem: "github-actions"
@@ -36,3 +64,4 @@ updates:
directory: "/"
schedule:
interval: "weekly"
day: "monday"
+24 -7
View File
@@ -1,10 +1,10 @@
# Add 'kernel' label to any change within Connectors, Extensions, Skills, and tests directories
kernel:
- "dotnet/src/Connectors/**/*"
- "dotnet/src/Extensions/**/*"
- "dotnet/src/Skills/**/*"
- "dotnet/src/IntegrationTests/**/*"
- "dotnet/src/SemanticKernel.UnitTests/**/*"
- dotnet/src/Connectors/**/*
- dotnet/src/Extensions/**/*
- dotnet/src/Skills/**/*
- dotnet/src/IntegrationTests/**/*
- dotnet/src/SemanticKernel.UnitTests/**/*
# Add 'kernel.core' label to any change within the 'SemanticKernel', 'SemanticKernel.Abstractions', or 'SemanticKernel.MetaPackage' directories
kernel.core:
@@ -16,11 +16,28 @@ kernel.core:
python:
- python/**/*
# Add 'java' label to any change within the 'java' directory
java:
- java/**/*
# Add 'samples' label to any change within the 'samples' directory
samples:
- samples/**/*
# Add '.NET' label to any change within samples or kernel 'dotnet' directories.
.NET:
- dotnet/src/**/*
- samples/**/dotnet/**/*
- dotnet/**/*
# Add 'copilot chat' label to any change within the 'samples/apps/copilot-chat-app' directory
copilot chat:
- samples/apps/copilot-chat-app/**/*
# Add 'documentation' label to any change within the 'docs' directory, or any '.md' files
documentation:
- docs/**/*
- '**/*.md'
# Add 'memory' label to any memory connectors in dotnet/ or python/
memory:
- dotnet/src/Connectors/Connectors.Memory.*/**/*
- python/semantic_kernel/connectors/memory/**/*
+71 -60
View File
@@ -2,9 +2,9 @@ name: Copilot Chat Tests
on:
workflow_dispatch:
push:
branches: [ "main", "feature*" ]
paths:
- 'samples/apps/copilot-chat-app/**'
branches: ["main", "feature*"]
paths:
- "samples/apps/copilot-chat-app/**"
permissions:
contents: read
@@ -17,70 +17,81 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache-dependency-path: samples/apps/copilot-chat-app/webapp/yarn.lock
cache: 'yarn'
- uses: actions/setup-node@v3
with:
node-version: 16
cache-dependency-path: samples/apps/copilot-chat-app/webapp/yarn.lock
cache: "yarn"
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Install dependencies
run: yarn install
- name: Install dependencies
run: yarn install
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Update AIService configuration
working-directory: samples/apps/copilot-chat-app/webapi
env:
AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
run: |
dotnet dev-certs https
dotnet user-secrets set "AIService:Key" "$AzureOpenAI__ApiKey"
dotnet user-secrets set "AIService:Endpoint" "$AzureOpenAI__Endpoint"
- name: Update AIService configuration
working-directory: samples/apps/copilot-chat-app/webapi
env:
AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
run: |
dotnet dev-certs https
dotnet user-secrets set "AIService:Key" "$AzureOpenAI__ApiKey"
dotnet user-secrets set "AIService:Endpoint" "$AzureOpenAI__Endpoint"
- name: Start service in background
working-directory: samples/apps/copilot-chat-app/webapi
run: |
dotnet run > service-log.txt 2>&1 &
for attempt in {0..20}; do
jobs
echo 'Waiting for service to start...';
if curl -k https://localhost:40443/healthz; then
echo;
echo 'Service started';
break;
fi;
- name: Start service in background
working-directory: samples/apps/copilot-chat-app/webapi
run: |
dotnet run > service-log.txt 2>&1 &
for attempt in {0..20}; do
jobs
echo 'Waiting for service to start...';
if curl -k https://localhost:40443/healthz; then
echo;
echo 'Service started';
break;
fi;
sleep 5;
done
sleep 5;
done
- name: Run Playwright tests
env:
REACT_APP_BACKEND_URI: https://localhost:40443/
REACT_APP_AAD_CLIENT_ID: ${{ secrets.COPILOT_CHAT_REACT_APP_AAD_CLIENT_ID }}
REACT_APP_AAD_AUTHORITY: https://login.microsoftonline.com/common
REACT_APP_TEST_USER_ACCOUNT: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT }}
REACT_APP_TEST_USER_PASSWORD: ${{ secrets.COPILOT_CHAT_TEST_USER_PASSWORD }}
run: yarn playwright test
- name: Run Playwright tests
env:
REACT_APP_BACKEND_URI: https://localhost:40443/
REACT_APP_AAD_CLIENT_ID: ${{ secrets.COPILOT_CHAT_REACT_APP_AAD_CLIENT_ID }}
REACT_APP_AAD_AUTHORITY: https://login.microsoftonline.com/common
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: samples/apps/copilot-chat-app/webapp/playwright-report/
retention-days: 30
REACT_APP_TEST_USER_ACCOUNT1: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT1 }}
REACT_APP_TEST_USER_ACCOUNT1_INITIALS: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT1_INITIALS }}
REACT_APP_TEST_USER_ACCOUNT2: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT2 }}
REACT_APP_TEST_USER_PASSWORD: ${{ secrets.COPILOT_CHAT_TEST_USER_PASSWORD }}
- uses: actions/upload-artifact@v3
if: always()
with:
name: service-log
path: samples/apps/copilot-chat-app/webapi/service-log.txt
retention-days: 30
REACT_APP_TEST_JIRA_EMAIL: ${{ secrets.COPILOT_CHAT_TEST_JIRA_EMAIL }}
REACT_APP_TEST_JIRA_ACCESS_TOKEN: ${{ secrets.COPILOT_CHAT_TEST_JIRA_ACCESS_TOKEN }}
REACT_APP_TEST_JIRA_SERVER_URL: ${{ secrets.COPILOT_CHAT_TEST_JIRA_SERVER_URL }}
REACT_APP_TEST_GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REACT_APP_TEST_GITHUB_ACCOUNT_OWNER: ${{ secrets.COPILOT_CHAT_TEST_GITHUB_ACCOUNT_OWNER }}
REACT_APP_TEST_GITHUB_REPOSITORY_NAME: ${{ secrets.COPILOT_CHAT_TEST_GITHUB_REPOSITORY_NAME }}
run: yarn playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: samples/apps/copilot-chat-app/webapp/playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v3
if: always()
with:
name: service-log
path: samples/apps/copilot-chat-app/webapi/service-log.txt
retention-days: 30
-2
View File
@@ -11,8 +11,6 @@ on:
paths:
- 'dotnet/**'
- 'samples/dotnet/**'
- '**.cs'
- '**.csproj'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+9 -1
View File
@@ -19,6 +19,14 @@ jobs:
pull-requests: write
steps:
- name: Check for command
id: command
uses: xt0rted/slash-command-action@v2
continue-on-error: true
with:
command: spotless
reaction-type: "eyes"
- name: Get command
env:
BODY: ${{ github.event.comment.body }}
@@ -77,4 +85,4 @@ jobs:
"
gh pr comment $NUMBER --body "$body"
fi
working-directory: java
working-directory: java
+45
View File
@@ -0,0 +1,45 @@
name: Label issues
on:
issues:
types:
- reopened
- opened
jobs:
label_issues:
name: "Issue: add labels"
if: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }}
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/github-script@v6
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
// Get the issue body and title
const body = context.payload.issue.body
let title = context.payload.issue.title
// Define the labels array
let labels = ["triage"]
// Check if the body or the title contains the word 'python' (case-insensitive)
if (body.match(/python/i) || title.match(/python/i)) {
// Add the 'python' label to the array
labels.push("python")
}
// Check if the body or the title contains the word 'java' (case-insensitive)
if (body.match(/java/i) || title.match(/java/i)) {
// Add the 'java' label to the array
labels.push("java")
}
// Add the labels to the issue
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: labels
});
@@ -5,11 +5,11 @@
# file with configuration. For more information, see:
# https://github.com/actions/labeler
name: Labeler
name: Label pull request
on: [pull_request_target]
jobs:
label:
add_label:
runs-on: ubuntu-latest
permissions:
contents: read
@@ -18,4 +18,4 @@ jobs:
steps:
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}"
+74
View File
@@ -0,0 +1,74 @@
name: Label title prefix
on:
issues:
types: [ labeled ]
pull_request_target:
types: [ labeled ]
jobs:
add_title_prefix:
name: "Issue/PR: add title prefix"
continue-on-error: true
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/github-script@v6
name: "Issue/PR: update title"
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
let prefixLabels = {
"python": "Python",
"java": "Java",
".NET": ".Net",
"copilot chat": "Copilot Chat"
};
function addTitlePrefix(title, prefix)
{
// Update the title based on the label and prefix
// Check if the title starts with the prefix (case-sensitive)
if (!title.startsWith(prefix + ": ")) {
// If not, check if the first word is the label (case-insensitive)
if (title.match(new RegExp(`^${prefix}`, 'i'))) {
// If yes, replace it with the prefix (case-sensitive)
title = title.replace(new RegExp(`^${prefix}`, 'i'), prefix);
} else {
// If not, prepend the prefix to the title
title = prefix + ": " + title;
}
}
return title;
}
labelAdded = context.payload.label.name
// Check if the issue or PR has the label
if (labelAdded in prefixLabels) {
let prefix = prefixLabels[labelAdded];
switch(context.eventName) {
case 'issues':
github.rest.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
title: addTitlePrefix(context.payload.issue.title, prefix)
});
break
case 'pull_request_target':
github.rest.pulls.update({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
title: addTitlePrefix(context.payload.pull_request.title, prefix)
});
break
default:
core.setFailed('Unrecognited eventName: ' + context.eventName);
}
}
+45 -45
View File
@@ -7,12 +7,11 @@ name: Python Integration Tests
on:
workflow_dispatch:
push:
branches: [ "main"]
paths:
- 'python/**'
branches: ["main"]
paths:
- "python/**"
schedule:
- cron: '0 */12 * * *' # Run every 12 hours: midnight UTC and noon UTC
- cron: "0 */12 * * *" # Run every 12 hours: midnight UTC and noon UTC
permissions:
contents: read
@@ -25,44 +24,45 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
os: [ ubuntu-latest, windows-latest, macos-latest ]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies wtih hnswlib native disabled
if: matrix.os == 'macos-latest' && matrix.python-version == '3.11'
run: |
export HNSWLIB_NO_NATIVE=1
python -m pip install --upgrade pip setuptools wheel
python -m pip install poetry pytest
cd python && poetry install --with hugging_face --with chromadb --with weaviate
- name: Install dependencies wtih hnswlib native enabled
if: matrix.os != 'macos-latest' || matrix.python-version != '3.11'
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install poetry pytest
cd python && poetry install --with hugging_face --with chromadb --with weaviate
- name: Run Integration Tests
shell: bash
env: # Set Azure credentials secret as an input
HNSWLIB_NO_NATIVE: 1
Python_Integration_Tests: Python_Integration_Tests
AzureOpenAI__Label: azure-text-davinci-003
AzureOpenAIEmbedding__Label: azure-text-embedding-ada-002
AzureOpenAI__DeploymentName: ${{ vars.AZUREOPENAI__DEPLOYMENTNAME }}
AzureOpenAIChat__DeploymentName: ${{ vars.AZUREOPENAI__CHAT__DEPLOYMENTNAME }}
AzureOpenAIEmbeddings__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDING__DEPLOYMENTNAME }}
AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
AzureOpenAIEmbeddings__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
AzureOpenAIEmbeddings__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
Bing__ApiKey: ${{ secrets.BING__APIKEY }}
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
run: |
cd python
poetry run pytest ./tests/integration
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies with hnswlib native disabled
if: matrix.os == 'macos-latest' && matrix.python-version == '3.11'
run: |
export HNSWLIB_NO_NATIVE=1
python -m pip install --upgrade pip setuptools wheel
python -m pip install poetry pytest
cd python && poetry install --with hugging_face --with chromadb --with weaviate
- name: Install dependencies with hnswlib native enabled
if: matrix.os != 'macos-latest' || matrix.python-version != '3.11'
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install poetry pytest
cd python && poetry install --with hugging_face --with chromadb --with weaviate
- name: Run Integration Tests
shell: bash
env: # Set Azure credentials secret as an input
HNSWLIB_NO_NATIVE: 1
Python_Integration_Tests: Python_Integration_Tests
AzureOpenAI__Label: azure-text-davinci-003
AzureOpenAIEmbedding__Label: azure-text-embedding-ada-002
AzureOpenAI__DeploymentName: ${{ vars.AZUREOPENAI__DEPLOYMENTNAME }}
AzureOpenAIChat__DeploymentName: ${{ vars.AZUREOPENAI__CHAT__DEPLOYMENTNAME }}
AzureOpenAIEmbeddings__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDING__DEPLOYMENTNAME }}
AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
AzureOpenAIEmbeddings__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
AzureOpenAIEmbeddings__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
Bing__ApiKey: ${{ secrets.BING__APIKEY }}
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
Pinecone__ApiKey: ${{ secrets.PINECONE__APIKEY }}
Pinecone__Environment: ${{ secrets.PINECONE__ENVIRONMENT }}
run: |
cd python
poetry run pytest ./tests/integration
+2 -1
View File
@@ -476,4 +476,5 @@ playwright-report/
# Static Web App deployment config
swa-cli.config.json
**/copilot-chat-app/webapp/build
**/copilot-chat-app/webapp/build
**/copilot-chat-app/webapp/node_modules
+2 -1
View File
@@ -89,5 +89,6 @@
],
},
"java.debug.settings.onBuildFailureProceed": true,
"java.compile.nullAnalysis.mode": "disabled"
"java.compile.nullAnalysis.mode": "disabled",
"dotnet.defaultSolution": "dotnet\\SK-dotnet.sln"
}
+9 -9
View File
@@ -39,21 +39,21 @@
|-----------------------------------|:---:|:------:|:----:|-------|
| TextMemory Skill | ✅ | ✅ | 🔄 | |
| ConversationSummary Skill | ✅ | ✅ | ❌ | |
| FileIO Skill | ✅ | ✅ | ❌ | |
| Http Skill | ✅ | ✅ | ❌ | |
| Math Skill | ✅ | ✅ | ❌ | |
| FileIO Skill | ✅ | ✅ | ✅ | |
| Http Skill | ✅ | ✅ | ✅ | |
| Math Skill | ✅ | ✅ | ✅ | |
| Text Skill | ✅ | ✅ | 🔄 | |
| Time Skill | ✅ | ✅ | 🔄 | |
| Wait Skill | ✅ | ❌ | ❌ | |
| Time Skill | ✅ | ✅ | ✅ | |
| Wait Skill | ✅ | ❌ | ✅ | |
## Planning
| | C# | Python | Java | Notes |
|-----------------------------------|:---:|:------:|:----:|-------|
| Plan | ✅ | 🔄 | ❌ | Plan object model to be completed |
| BasicPlanner | ❌ | ✅ | ❌ | |
| ActionPlanner | ✅ | ❌ | ❌ | |
| SequentialPlanner | ✅ | ❌ | ❌ | |
| Plan | ✅ | 🔄 | ❌ | Plan object model to be completed |
| BasicPlanner | ❌ | ✅ | ❌ | |
| ActionPlanner | ✅ | ❌ | 🔄 | |
| SequentialPlanner | ✅ | ❌ | 🔄 | |
## Memory Connectors, Vector storage
+25 -14
View File
@@ -2,11 +2,12 @@
[![Python package](https://img.shields.io/pypi/v/semantic-kernel)](https://pypi.org/project/semantic-kernel/)
[![Nuget package](https://img.shields.io/nuget/vpre/Microsoft.SemanticKernel)](https://www.nuget.org/packages/Microsoft.SemanticKernel/)
[![dotnet](https://github.com/microsoft/semantic-kernel/actions/workflows/dotnet-ci.yml/badge.svg?branch=main)](https://github.com/microsoft/semantic-kernel/actions/workflows/dotnet-ci.yml)
[![dotnet Docker](https://github.com/microsoft/semantic-kernel/actions/workflows/dotnet-ci-docker.yml/badge.svg?branch=main)](https://github.com/microsoft/semantic-kernel/actions/workflows/dotnet-ci-docker.yml)
[![dotnet Windows](https://github.com/microsoft/semantic-kernel/actions/workflows/dotnet-ci-windows.yml/badge.svg?branch=main)](https://github.com/microsoft/semantic-kernel/actions/workflows/dotnet-ci-windows.yml)
[![License: MIT](https://img.shields.io/github/license/microsoft/semantic-kernel)](https://github.com/microsoft/semantic-kernel/blob/main/LICENSE)
[![Discord](https://img.shields.io/discord/1063152441819942922?label=Discord&logo=discord&logoColor=white&color=d82679)](https://aka.ms/SKDiscord)
> ℹ️ **NOTE**:This project is just like AI and will evolve quickly.
> ℹ️ **NOTE**: This project is just like AI and will evolve quickly.
> We invite you to join us in developing the Semantic Kernel together!
> Please contribute by
> using GitHub [Discussions](https://github.com/microsoft/semantic-kernel/discussions),
@@ -43,16 +44,24 @@ in the history of computing.
Semantic Kernel is available to explore AI and build apps with C# and Python:
<div style="display:flex;height:30px;padding:5px 0 5px 10px;">
<img src="https://user-images.githubusercontent.com/371009/230673036-fad1e8e6-5d48-49b1-a9c1-6f9834e0d165.png" style="margin-right:12px" height="30"/>
<a href="dotnet/README.md">Using Semantic Kernel in C#</a>.
</div>
<div style="display:flex;height:30px;padding:5px 0 5px 10px;">
<img src="https://user-images.githubusercontent.com/371009/230673733-7a447d30-b48e-46e1-bd84-2b321c90649e.png" style="margin-right:12px" height="30"/>
<a href="python/README.md">Using Semantic Kernel in Python</a>.
</div>
<br/>
<table width=100%>
<tbody>
<tr>
<td>
<img align="left" width=52px src="https://user-images.githubusercontent.com/371009/230673036-fad1e8e6-5d48-49b1-a9c1-6f9834e0d165.png">
<div>
<a href="dotnet/README.md">Using Semantic Kernel in C#</a> &nbsp
</div>
</td>
<td>
<img align="left" width=52px src="https://raw.githubusercontent.com/devicons/devicon/master/icons/python/python-original.svg">
<div>
<a href="python/README.md">Using Semantic Kernel in Python</a>
</div>
</td>
</tr>
</tbody>
</table>
See the [Feature Matrix](FEATURE_MATRIX.md) to see a breakdown of feature parity between C# and Python.
@@ -60,14 +69,16 @@ The quickest way to get started with the basics is to get an API key
(OpenAI or Azure OpenAI)
and to run one of the C# or Python console applications/scripts:
For C#:
### For C#:
1. Create a new console app.
2. Add the semantic kernel nuget `Microsoft.SemanticKernel`.
3. Copy the code from [here](dotnet/README.md) into the app `Program.cs` file.
4. Replace the configuration placeholders for API key and other params with your key and settings.
5. Run with `F5` or `dotnet run`
For Python:
### For Python:
1. Install the pip package: `python -m pip install semantic-kernel`.
2. Create a new script e.g. `hello-world.py`.
3. Store your API key and settings in an `.env` file as described [here](python/README.md).
-61
View File
@@ -1,61 +0,0 @@
trigger:
- main
# no PR triggers
pr: none
pool:
vmImage: 'windows-latest'
steps:
- task: CredScan@3
- task: ESLint@1
inputs:
Configuration: 'recommended'
TargetType: 'eslint'
ErrorLevel: 'warn'
- task: UseDotNet@2
displayName: 'Use .NET 7.0'
inputs:
packageType: 'sdk'
version: '7.x'
- task: Semmle@1
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
inputs:
sourceCodeDirectory: '$(Build.SourcesDirectory)'
language: 'csharp'
buildCommandsString: '$(Agent.ToolsDirectory)\dotnet\dotnet.exe restore $(BUILD.SourcesDirectory)\dotnet\SK-dotnet.sln#$(Agent.ToolsDirectory)\dotnet\dotnet.exe build $(BUILD.SourcesDirectory)\dotnet\SK-dotnet.sln'
querySuite: 'Recommended'
timeout: '1800'
ram: '16384'
addProjectDirToScanningExclusionList: true
# Usage of System.AccessToken is only required for uploading
# results to CodeQL servers via variable LGTM.UploadSnapshot = true
# If you want to analyze errors on your own or if LGTM.UploadSnapshot = false,
# then passing this environment variable is not required.
#######################################################
# Highly Discouraged, only for backward compatibility #
#######################################################
# When code is hosted on GitHub and build pipeline is created on ADO then a PAT token can be used in place of AccessToken as part of environment variable
# SYSTEM_ACCESSTOKEN: $(PATToken)
# Where PATToken is name of the variable and value contains the actual PAT token generated by user
- task: ComponentGovernanceComponentDetection@0
inputs:
scanType: 'Register'
verbosity: 'Verbose'
alertWarningLevel: 'High'
failOnAlert: true
- task: PublishSecurityAnalysisLogs@3
inputs:
ArtifactName: 'CodeAnalysisLogs'
ArtifactType: 'Container'
AllTools: true
ToolLogsNotFoundAction: 'Standard'
+16 -16
View File
@@ -9,34 +9,34 @@
<PackageVersion Include="Azure.Identity" Version="1.9.0" />
<PackageVersion Include="Azure.Search.Documents" Version="11.5.0-beta.2" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="[1.1.0, )" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="[7.0.0, )" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="NRedisStack" Version="0.7.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageVersion Include="NRedisStack" Version="0.8.0" />
<PackageVersion Include="Pgvector" Version="0.1.3" />
<PackageVersion Include="Polly" Version="7.2.4" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Json" Version="[7.0.2, )" />
<PackageVersion Include="System.Text.Json" Version="6.0.8" />
<!-- Microsoft.Extensions.Logging -->
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="[7.0.0, )" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="[7.0.0, )" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<!-- Microsoft.Extensions.Configuration -->
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0-preview.5.23280.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<!-- Test -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="xunit" Version="2.4.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<!-- Skills -->
<PackageVersion Include="DocumentFormat.OpenXml" Version="2.20.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="[7.0, )" />
<PackageVersion Include="DuckDB.NET.Data.Full" Version="[0.8, )" />
<PackageVersion Include="DuckDB.NET.Data" Version="[0.8, )" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.8" />
<PackageVersion Include="DuckDB.NET.Data.Full" Version="0.8.1" />
<PackageVersion Include="DuckDB.NET.Data" Version="0.8.1" />
<PackageVersion Include="Microsoft.Graph" Version="[4.51.0, 5)" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="[3.32.3, )" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="[2.28.0, )" />
@@ -44,7 +44,7 @@
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="[1.6.3, )" />
<PackageVersion Include="Newtonsoft.Json" Version="[13.0.3, )" />
<PackageVersion Include="Google.Apis.CustomSearchAPI.v1" Version="[1.60.0.3001, )" />
<PackageVersion Include="Grpc.Net.Client" Version="2.54.0" />
<PackageVersion Include="Grpc.Net.Client" Version="2.55.0" />
<PackageVersion Include="protobuf-net" Version="3.2.26" />
<PackageVersion Include="protobuf-net.Reflection" Version="3.2.12" />
<PackageVersion Include="CoreCLR-NCalc" Version="2.2.113" />
-38
View File
@@ -13,8 +13,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{FA37
..\samples\dotnet\Directory.Packages.props = ..\samples\dotnet\Directory.Packages.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoadPromptsFromCloud", "..\samples\dotnet\kernel-extension-load-prompts-from-cloud\LoadPromptsFromCloud.csproj", "{A05BF65E-085E-476C-B88A-9DA93F005416}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KernelSyntaxExamples", "samples\KernelSyntaxExamples\KernelSyntaxExamples.csproj", "{47C6F821-5103-431F-B3B8-A2868A68BB78}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MsGraphSkillsExample", "..\samples\dotnet\graph-api-skills\MsGraphSkillsExample.csproj", "{3EB61E99-C39B-4620-9482-F8DA18E48525}"
@@ -52,18 +50,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuget", "nuget", "{F4243136
nuget\NUGET.md = nuget\NUGET.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitHubSkills", "..\samples\dotnet\github-skills\GitHubSkills.csproj", "{39E5F0F6-8B36-4ECA-A5F6-FC7522DC2ECF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Skills.OpenAPI", "src\Skills\Skills.OpenAPI\Skills.OpenAPI.csproj", "{F2A1F81E-700E-4C0E-B021-B9EF29AA20BD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileCompression", "..\samples\dotnet\FileCompression\FileCompression.csproj", "{BC70A5D8-2125-4C37-8C0E-C903EAFA9772}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "connectors", "connectors", "{0247C2C9-86C3-45BA-8873-28B0948EDC0C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.UnitTests", "src\Connectors\Connectors.UnitTests\Connectors.UnitTests.csproj", "{EB3FC57F-E591-4C88-BCD5-B6A1BC635168}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CopilotChatWebApi", "..\samples\apps\copilot-chat-app\webapi\CopilotChatWebApi.csproj", "{CCABF515-2C79-453E-A5A2-69C69B8D172E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Qdrant", "src\Connectors\Connectors.Memory.Qdrant\Connectors.Memory.Qdrant.csproj", "{5DEBAA62-F117-496A-8778-FED3604B70E2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Sqlite", "src\Connectors\Connectors.Memory.Sqlite\Connectors.Memory.Sqlite.csproj", "{EC004F12-2F60-4EDD-B3CD-3A504900D929}"
@@ -82,8 +74,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.Abstractions
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.MetaPackage", "src\SemanticKernel.MetaPackage\SemanticKernel.MetaPackage.csproj", "{E3299033-EB81-4C4C-BCD9-E8DC40937969}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImportDocument", "..\samples\apps\copilot-chat-app\importdocument\ImportDocument.csproj", "{94E14913-D600-4448-A5C5-A268C23B2C3B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Planning.ActionPlanner", "src\Extensions\Planning.ActionPlanner\Planning.ActionPlanner.csproj", "{994BEF0B-E277-4D10-BB13-FE670D26620D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "extensions", "extensions", "{078F96B4-09E1-4E0E-B214-F71A4F4BF633}"
@@ -165,11 +155,6 @@ Global
{A284C7EB-2248-4A75-B112-F5DCDE65410D}.Publish|Any CPU.Build.0 = Publish|Any CPU
{A284C7EB-2248-4A75-B112-F5DCDE65410D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A284C7EB-2248-4A75-B112-F5DCDE65410D}.Release|Any CPU.Build.0 = Release|Any CPU
{A05BF65E-085E-476C-B88A-9DA93F005416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A05BF65E-085E-476C-B88A-9DA93F005416}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A05BF65E-085E-476C-B88A-9DA93F005416}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{A05BF65E-085E-476C-B88A-9DA93F005416}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A05BF65E-085E-476C-B88A-9DA93F005416}.Release|Any CPU.Build.0 = Release|Any CPU
{47C6F821-5103-431F-B3B8-A2868A68BB78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47C6F821-5103-431F-B3B8-A2868A68BB78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47C6F821-5103-431F-B3B8-A2868A68BB78}.Publish|Any CPU.ActiveCfg = Release|Any CPU
@@ -221,31 +206,18 @@ Global
{107156B4-5A8B-45C7-97A2-4544D7FA19DE}.Publish|Any CPU.Build.0 = Release|Any CPU
{107156B4-5A8B-45C7-97A2-4544D7FA19DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{107156B4-5A8B-45C7-97A2-4544D7FA19DE}.Release|Any CPU.Build.0 = Release|Any CPU
{39E5F0F6-8B36-4ECA-A5F6-FC7522DC2ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39E5F0F6-8B36-4ECA-A5F6-FC7522DC2ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39E5F0F6-8B36-4ECA-A5F6-FC7522DC2ECF}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{39E5F0F6-8B36-4ECA-A5F6-FC7522DC2ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39E5F0F6-8B36-4ECA-A5F6-FC7522DC2ECF}.Release|Any CPU.Build.0 = Release|Any CPU
{F2A1F81E-700E-4C0E-B021-B9EF29AA20BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2A1F81E-700E-4C0E-B021-B9EF29AA20BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2A1F81E-700E-4C0E-B021-B9EF29AA20BD}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{F2A1F81E-700E-4C0E-B021-B9EF29AA20BD}.Publish|Any CPU.Build.0 = Publish|Any CPU
{F2A1F81E-700E-4C0E-B021-B9EF29AA20BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2A1F81E-700E-4C0E-B021-B9EF29AA20BD}.Release|Any CPU.Build.0 = Release|Any CPU
{BC70A5D8-2125-4C37-8C0E-C903EAFA9772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC70A5D8-2125-4C37-8C0E-C903EAFA9772}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC70A5D8-2125-4C37-8C0E-C903EAFA9772}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{BC70A5D8-2125-4C37-8C0E-C903EAFA9772}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC70A5D8-2125-4C37-8C0E-C903EAFA9772}.Release|Any CPU.Build.0 = Release|Any CPU
{EB3FC57F-E591-4C88-BCD5-B6A1BC635168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB3FC57F-E591-4C88-BCD5-B6A1BC635168}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB3FC57F-E591-4C88-BCD5-B6A1BC635168}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{EB3FC57F-E591-4C88-BCD5-B6A1BC635168}.Publish|Any CPU.Build.0 = Release|Any CPU
{EB3FC57F-E591-4C88-BCD5-B6A1BC635168}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB3FC57F-E591-4C88-BCD5-B6A1BC635168}.Release|Any CPU.Build.0 = Release|Any CPU
{CCABF515-2C79-453E-A5A2-69C69B8D172E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCABF515-2C79-453E-A5A2-69C69B8D172E}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{CCABF515-2C79-453E-A5A2-69C69B8D172E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5DEBAA62-F117-496A-8778-FED3604B70E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DEBAA62-F117-496A-8778-FED3604B70E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DEBAA62-F117-496A-8778-FED3604B70E2}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
@@ -299,11 +271,6 @@ Global
{E3299033-EB81-4C4C-BCD9-E8DC40937969}.Publish|Any CPU.Build.0 = Publish|Any CPU
{E3299033-EB81-4C4C-BCD9-E8DC40937969}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3299033-EB81-4C4C-BCD9-E8DC40937969}.Release|Any CPU.Build.0 = Release|Any CPU
{94E14913-D600-4448-A5C5-A268C23B2C3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94E14913-D600-4448-A5C5-A268C23B2C3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94E14913-D600-4448-A5C5-A268C23B2C3B}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{94E14913-D600-4448-A5C5-A268C23B2C3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94E14913-D600-4448-A5C5-A268C23B2C3B}.Release|Any CPU.Build.0 = Release|Any CPU
{994BEF0B-E277-4D10-BB13-FE670D26620D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{994BEF0B-E277-4D10-BB13-FE670D26620D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{994BEF0B-E277-4D10-BB13-FE670D26620D}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
@@ -386,7 +353,6 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A284C7EB-2248-4A75-B112-F5DCDE65410D} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{A05BF65E-085E-476C-B88A-9DA93F005416} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{47C6F821-5103-431F-B3B8-A2868A68BB78} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{3EB61E99-C39B-4620-9482-F8DA18E48525} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{34A7F1EF-D243-4160-A413-D713FEABCD94} = {FA3720F1-C99A-49B2-9577-A940257098BF}
@@ -398,12 +364,9 @@ Global
{9ECD1AA0-75B3-4E25-B0B5-9F0945B64974} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{107156B4-5A8B-45C7-97A2-4544D7FA19DE} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
{F4243136-252A-4459-A7C4-EE8C056D6B0B} = {158A4E5E-AEE0-4D60-83C7-8E089B2D881D}
{39E5F0F6-8B36-4ECA-A5F6-FC7522DC2ECF} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{F2A1F81E-700E-4C0E-B021-B9EF29AA20BD} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
{BC70A5D8-2125-4C37-8C0E-C903EAFA9772} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{0247C2C9-86C3-45BA-8873-28B0948EDC0C} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{EB3FC57F-E591-4C88-BCD5-B6A1BC635168} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{CCABF515-2C79-453E-A5A2-69C69B8D172E} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{5DEBAA62-F117-496A-8778-FED3604B70E2} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{EC004F12-2F60-4EDD-B3CD-3A504900D929} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{EA61C289-7928-4B78-A9C1-7AAD61F907CD} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
@@ -413,7 +376,6 @@ Global
{AFA81EB7-F869-467D-8A90-744305D80AAC} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{627742DB-1E52-468A-99BD-6FF1A542D25B} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{E3299033-EB81-4C4C-BCD9-E8DC40937969} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{94E14913-D600-4448-A5C5-A268C23B2C3B} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{994BEF0B-E277-4D10-BB13-FE670D26620D} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
{078F96B4-09E1-4E0E-B214-F71A4F4BF633} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{F51017A9-15C8-472D-893C-080046D710A6} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
View File
Executable → Regular
View File
@@ -20,7 +20,7 @@ public static class Example39_Postgres
dataSourceBuilder.UseVector();
using NpgsqlDataSource dataSource = dataSourceBuilder.Build();
PostgresMemoryStore memoryStore = new(dataSource, vectorSize: 1536, schema: "public", numberOfLists: 100);
PostgresMemoryStore memoryStore = new(dataSource, vectorSize: 1536, schema: "public");
IKernel kernel = Kernel.Builder
.WithLogger(ConsoleLogger.Log)
@@ -44,6 +44,10 @@
<ProjectReference Include="..\..\src\Skills\Skills.Web\Skills.Web.csproj" />
<ProjectReference Include="..\..\src\SemanticKernel\SemanticKernel.csproj" />
<ProjectReference Include="..\NCalcSkills\NCalcSkills.csproj" />
<!-- Because some of the referenced projects have dependencies that themselves have System.Text.Json set with a minimum of 7.0. -->
<PackageReference Include="System.Text.Json" />
<PackageVersion Update="System.Text.Json" Version="7.0.3" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\30-user-prompt.txt" />
@@ -347,9 +347,9 @@ public abstract class ClientBase
return validRole;
}
private static void ValidateMaxTokens(int maxTokens)
private static void ValidateMaxTokens(int? maxTokens)
{
if (maxTokens < 1)
if (maxTokens.HasValue && maxTokens < 1)
{
throw new AIException(
AIException.ErrorCodes.InvalidRequest,
@@ -3,7 +3,6 @@
using System.Net.Http;
using System.Text;
using System.Text.Json.Serialization;
using Microsoft.SemanticKernel.Connectors.Memory.Pinecone.Http;
namespace Microsoft.SemanticKernel.Connectors.Memory.Pinecone.Model;
@@ -20,6 +20,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="Pgvector" />
<!-- pgvector pulls in a 7.0 version -->
<PackageVersion Update="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -13,76 +14,95 @@ namespace Microsoft.SemanticKernel.Connectors.Memory.Postgres;
public interface IPostgresDbClient
{
/// <summary>
/// Check if a collection exists.
/// Check if a table exists.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
Task<bool> DoesCollectionExistsAsync(string collectionName, CancellationToken cancellationToken = default);
Task<bool> DoesTableExistsAsync(string tableName, CancellationToken cancellationToken = default);
/// <summary>
/// Create a collection.
/// Create a table.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
Task CreateCollectionAsync(string collectionName, CancellationToken cancellationToken = default);
Task CreateTableAsync(string tableName, CancellationToken cancellationToken = default);
/// <summary>
/// Get all collections.
/// Get all tables.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
IAsyncEnumerable<string> GetCollectionsAsync(CancellationToken cancellationToken = default);
/// <returns>A group of tables.</returns>
IAsyncEnumerable<string> GetTablesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Delete a collection.
/// Delete a table.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
Task DeleteCollectionAsync(string collectionName, CancellationToken cancellationToken = default);
Task DeleteTableAsync(string tableName, CancellationToken cancellationToken = default);
/// <summary>
/// Upsert entry into a collection.
/// Upsert entry into a table.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="key">The key of the entry to upsert.</param>
/// <param name="metadata">The metadata of the entry.</param>
/// <param name="embedding">The embedding of the entry.</param>
/// <param name="timestamp">The timestamp of the entry</param>
/// <param name="timestamp">The timestamp of the entry. Its 'DateTimeKind' must be <see cref="DateTimeKind.Utc"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
Task UpsertAsync(string collectionName, string key, string? metadata, Vector? embedding, long? timestamp, CancellationToken cancellationToken = default);
Task UpsertAsync(string tableName, string key, string? metadata, Vector? embedding, DateTime? timestamp, CancellationToken cancellationToken = default);
/// <summary>
/// Gets the nearest matches to the <see cref="Vector"/>.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="embeddingFilter">The <see cref="Vector"/> to compare the collection's embeddings with.</param>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="embedding">The <see cref="Vector"/> to compare the table's embeddings with.</param>
/// <param name="limit">The maximum number of similarity results to return.</param>
/// <param name="minRelevanceScore">The minimum relevance threshold for returned results.</param>
/// <param name="withEmbeddings">If true, the embeddings will be returned in the entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
IAsyncEnumerable<(PostgresMemoryEntry, double)> GetNearestMatchesAsync(string collectionName, Vector embeddingFilter, int limit, double minRelevanceScore = 0, bool withEmbeddings = false, CancellationToken cancellationToken = default);
/// <returns>An asynchronous stream of <see cref="PostgresMemoryEntry"/> objects that the nearest matches to the <see cref="Vector"/>.</returns>
IAsyncEnumerable<(PostgresMemoryEntry, double)> GetNearestMatchesAsync(string tableName, Vector embedding, int limit, double minRelevanceScore = 0, bool withEmbeddings = false, CancellationToken cancellationToken = default);
/// <summary>
/// Read a entry by its key.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="key">The key of the entry to read.</param>
/// <param name="withEmbeddings">If true, the embeddings will be returned in the entries.</param>
/// <param name="withEmbeddings">If true, the embeddings will be returned in the entry.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
Task<PostgresMemoryEntry?> ReadAsync(string collectionName, string key, bool withEmbeddings = false, CancellationToken cancellationToken = default);
Task<PostgresMemoryEntry?> ReadAsync(string tableName, string key, bool withEmbeddings = false, CancellationToken cancellationToken = default);
/// <summary>
/// Read multiple entries by their keys.
/// </summary>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="keys">The keys of the entries to read.</param>
/// <param name="withEmbeddings">If true, the embeddings will be returned in the entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous stream of <see cref="PostgresMemoryEntry"/> objects that match the given keys.</returns>
IAsyncEnumerable<PostgresMemoryEntry> ReadBatchAsync(string tableName, IEnumerable<string> keys, bool withEmbeddings = false, CancellationToken cancellationToken = default);
/// <summary>
/// Delete a entry by its key.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="key">The key of the entry to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
Task DeleteAsync(string collectionName, string key, CancellationToken cancellationToken = default);
Task DeleteAsync(string tableName, string key, CancellationToken cancellationToken = default);
/// <summary>
/// Delete multiple entries by their key.
/// </summary>
/// <param name="tableName">The name assigned to a table of entries.</param>
/// <param name="keys">The keys of the entries to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
Task DeleteBatchAsync(string tableName, IEnumerable<string> keys, CancellationToken cancellationToken = default);
}
@@ -2,10 +2,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Npgsql;
using NpgsqlTypes;
using Pgvector;
namespace Microsoft.SemanticKernel.Connectors.Memory.Postgres;
@@ -22,220 +24,249 @@ public class PostgresDbClient : IPostgresDbClient
/// <param name="dataSource">Postgres data source.</param>
/// <param name="schema">Schema of collection tables.</param>
/// <param name="vectorSize">Embedding vector size.</param>
/// <param name="numberOfLists">Specifies the number of lists for indexing. Higher values can improve recall but may impact performance. The default value is 1000. More info <see href="https://github.com/pgvector/pgvector#indexing"/></param>
public PostgresDbClient(NpgsqlDataSource dataSource, string schema, int vectorSize, int numberOfLists)
public PostgresDbClient(NpgsqlDataSource dataSource, string schema, int vectorSize)
{
this._dataSource = dataSource;
this._schema = schema;
this._vectorSize = vectorSize;
this._numberOfLists = numberOfLists;
}
/// <summary>
/// Check if a collection exists.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
public async Task<bool> DoesCollectionExistsAsync(
string collectionName,
CancellationToken cancellationToken = default)
/// <inheritdoc />
public async Task<bool> DoesTableExistsAsync(string tableName, CancellationToken cancellationToken = default)
{
using NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $@"
SELECT table_name
FROM information_schema.tables
WHERE table_schema = @schema
AND table_type = 'BASE TABLE'
AND table_name = '{collectionName}'";
cmd.Parameters.AddWithValue("@schema", this._schema);
using var dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
await using (connection)
{
return dataReader.GetString(dataReader.GetOrdinal("table_name")) == collectionName;
}
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $@"
SELECT table_name
FROM information_schema.tables
WHERE table_schema = @schema
AND table_type = 'BASE TABLE'
AND table_name = '{tableName}'";
cmd.Parameters.AddWithValue("@schema", this._schema);
return false;
}
using NpgsqlDataReader dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return dataReader.GetString(dataReader.GetOrdinal("table_name")) == tableName;
}
/// <summary>
/// Create a collection.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
public async Task CreateCollectionAsync(string collectionName, CancellationToken cancellationToken = default)
{
using NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await this.CreateTableAsync(connection, collectionName, cancellationToken).ConfigureAwait(false);
await this.CreateIndexAsync(connection, collectionName, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get all collections.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
public async IAsyncEnumerable<string> GetCollectionsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
using NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT table_name
FROM information_schema.tables
WHERE table_schema = @schema
AND table_type = 'BASE TABLE'";
cmd.Parameters.AddWithValue("@schema", this._schema);
using var dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
yield return dataReader.GetString(dataReader.GetOrdinal("table_name"));
return false;
}
}
/// <summary>
/// Delete a collection.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
public async Task DeleteCollectionAsync(string collectionName, CancellationToken cancellationToken = default)
/// <inheritdoc />
public async Task CreateTableAsync(string tableName, CancellationToken cancellationToken = default)
{
using NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $"DROP TABLE IF EXISTS {this.GetTableName(collectionName)}";
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
await using (connection)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $@"
CREATE TABLE IF NOT EXISTS {this.GetFullTableName(tableName)} (
key TEXT NOT NULL,
metadata JSONB,
embedding vector({this._vectorSize}),
timestamp TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (key))";
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Upsert entry into a collection.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="key">The key of the entry to upsert.</param>
/// <param name="metadata">The metadata of the entry.</param>
/// <param name="embedding">The embedding of the entry.</param>
/// <param name="timestamp">The timestamp of the entry</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
public async Task UpsertAsync(string collectionName, string key,
string? metadata, Vector? embedding, long? timestamp, CancellationToken cancellationToken = default)
/// <inheritdoc />
public async IAsyncEnumerable<string> GetTablesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
using NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $@"
INSERT INTO {this.GetTableName(collectionName)} (key, metadata, embedding, timestamp)
VALUES(@key, @metadata, @embedding, @timestamp)
ON CONFLICT (key)
DO UPDATE SET metadata=@metadata, embedding=@embedding, timestamp=@timestamp";
cmd.Parameters.AddWithValue("@key", key);
cmd.Parameters.AddWithValue("@metadata", NpgsqlTypes.NpgsqlDbType.Jsonb, metadata ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@embedding", embedding ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@timestamp", timestamp ?? (object)DBNull.Value);
await using (connection)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT table_name
FROM information_schema.tables
WHERE table_schema = @schema
AND table_type = 'BASE TABLE'";
cmd.Parameters.AddWithValue("@schema", this._schema);
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
using NpgsqlDataReader dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
yield return dataReader.GetString(dataReader.GetOrdinal("table_name"));
}
}
}
/// <summary>
/// Gets the nearest matches to the <see cref="Vector"/>.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="embeddingFilter">The <see cref="Vector"/> to compare the collection's embeddings with.</param>
/// <param name="limit">The maximum number of similarity results to return.</param>
/// <param name="minRelevanceScore">The minimum relevance threshold for returned results.</param>
/// <param name="withEmbeddings">If true, the embeddings will be returned in the entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
/// <inheritdoc />
public async Task DeleteTableAsync(string tableName, CancellationToken cancellationToken = default)
{
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using (connection)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $"DROP TABLE IF EXISTS {this.GetFullTableName(tableName)}";
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async Task UpsertAsync(string tableName, string key,
string? metadata, Vector? embedding, DateTime? timestamp, CancellationToken cancellationToken = default)
{
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using (connection)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $@"
INSERT INTO {this.GetFullTableName(tableName)} (key, metadata, embedding, timestamp)
VALUES(@key, @metadata, @embedding, @timestamp)
ON CONFLICT (key)
DO UPDATE SET metadata=@metadata, embedding=@embedding, timestamp=@timestamp";
cmd.Parameters.AddWithValue("@key", key);
cmd.Parameters.AddWithValue("@metadata", NpgsqlTypes.NpgsqlDbType.Jsonb, metadata ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@embedding", embedding ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@timestamp", NpgsqlTypes.NpgsqlDbType.TimestampTz, timestamp ?? (object)DBNull.Value);
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async IAsyncEnumerable<(PostgresMemoryEntry, double)> GetNearestMatchesAsync(
string collectionName, Vector embeddingFilter, int limit, double minRelevanceScore = 0, bool withEmbeddings = false,
string tableName, Vector embedding, int limit, double minRelevanceScore = 0, bool withEmbeddings = false,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var queryColumns = "key, metadata, timestamp";
string queryColumns = "key, metadata, timestamp";
if (withEmbeddings)
{
queryColumns = "*";
}
using NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = @$"
SELECT * FROM (SELECT {queryColumns}, 1 - (embedding <=> @embedding) AS cosine_similarity FROM {this.GetTableName(collectionName)}
) AS sk_memory_cosine_similarity_table
WHERE cosine_similarity >= @min_relevance_score
ORDER BY cosine_similarity DESC
Limit @limit";
cmd.Parameters.AddWithValue("@embedding", embeddingFilter);
cmd.Parameters.AddWithValue("@collection", collectionName);
cmd.Parameters.AddWithValue("@min_relevance_score", minRelevanceScore);
cmd.Parameters.AddWithValue("@limit", limit);
using var dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
await using (connection)
{
double cosineSimilarity = dataReader.GetDouble(dataReader.GetOrdinal("cosine_similarity"));
yield return (await this.ReadEntryAsync(dataReader, withEmbeddings, cancellationToken).ConfigureAwait(false), cosineSimilarity);
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = @$"
SELECT * FROM (SELECT {queryColumns}, 1 - (embedding <=> @embedding) AS cosine_similarity FROM {this.GetFullTableName(tableName)}
) AS sk_memory_cosine_similarity_table
WHERE cosine_similarity >= @min_relevance_score
ORDER BY cosine_similarity DESC
Limit @limit";
cmd.Parameters.AddWithValue("@embedding", embedding);
cmd.Parameters.AddWithValue("@collection", tableName);
cmd.Parameters.AddWithValue("@min_relevance_score", minRelevanceScore);
cmd.Parameters.AddWithValue("@limit", limit);
using NpgsqlDataReader dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
double cosineSimilarity = dataReader.GetDouble(dataReader.GetOrdinal("cosine_similarity"));
yield return (await this.ReadEntryAsync(dataReader, withEmbeddings, cancellationToken).ConfigureAwait(false), cosineSimilarity);
}
}
}
/// <summary>
/// Read a entry by its key.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="key">The key of the entry to read.</param>
/// <param name="withEmbeddings">If true, the embeddings will be returned in the entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
public async Task<PostgresMemoryEntry?> ReadAsync(string collectionName, string key,
/// <inheritdoc />
public async Task<PostgresMemoryEntry?> ReadAsync(string tableName, string key,
bool withEmbeddings = false, CancellationToken cancellationToken = default)
{
var queryColumns = "key, metadata, timestamp";
string queryColumns = "key, metadata, timestamp";
if (withEmbeddings)
{
queryColumns = "*";
}
using NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT {queryColumns} FROM {this.GetTableName(collectionName)} WHERE key=@key";
cmd.Parameters.AddWithValue("@key", key);
using var dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
await using (connection)
{
return await this.ReadEntryAsync(dataReader, withEmbeddings, cancellationToken).ConfigureAwait(false);
}
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT {queryColumns} FROM {this.GetFullTableName(tableName)} WHERE key=@key";
cmd.Parameters.AddWithValue("@key", key);
return null;
using NpgsqlDataReader dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return await this.ReadEntryAsync(dataReader, withEmbeddings, cancellationToken).ConfigureAwait(false);
}
return null;
}
}
/// <summary>
/// Delete a entry by its key.
/// </summary>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="key">The key of the entry to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
public async Task DeleteAsync(string collectionName, string key, CancellationToken cancellationToken = default)
/// <inheritdoc />
public async IAsyncEnumerable<PostgresMemoryEntry> ReadBatchAsync(string tableName, IEnumerable<string> keys, bool withEmbeddings = false,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
using NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
string[] keysArray = keys.ToArray();
if (keysArray.Length == 0)
{
yield break;
}
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $"DELETE FROM {this.GetTableName(collectionName)} WHERE key=@key";
cmd.Parameters.AddWithValue("@key", key);
string queryColumns = "key, metadata, timestamp";
if (withEmbeddings)
{
queryColumns = "*";
}
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using (connection)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT {queryColumns} FROM {this.GetFullTableName(tableName)} WHERE key=ANY(@keys)";
cmd.Parameters.AddWithValue("@keys", NpgsqlDbType.Array | NpgsqlDbType.Text, keysArray);
using NpgsqlDataReader dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
yield return await this.ReadEntryAsync(dataReader, withEmbeddings, cancellationToken).ConfigureAwait(false);
}
}
}
/// <inheritdoc />
public async Task DeleteAsync(string tableName, string key, CancellationToken cancellationToken = default)
{
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using (connection)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $"DELETE FROM {this.GetFullTableName(tableName)} WHERE key=@key";
cmd.Parameters.AddWithValue("@key", key);
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async Task DeleteBatchAsync(string tableName, IEnumerable<string> keys, CancellationToken cancellationToken = default)
{
string[] keysArray = keys.ToArray();
if (keysArray.Length == 0)
{
return;
}
NpgsqlConnection connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using (connection)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $"DELETE FROM {this.GetFullTableName(tableName)} WHERE key=ANY(@keys)";
cmd.Parameters.AddWithValue("@keys", NpgsqlDbType.Array | NpgsqlDbType.Text, keysArray);
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}
#region private ================================================================================
@@ -243,7 +274,6 @@ public class PostgresDbClient : IPostgresDbClient
private readonly NpgsqlDataSource _dataSource;
private readonly int _vectorSize;
private readonly string _schema;
private readonly int _numberOfLists;
/// <summary>
/// Read a entry.
@@ -257,54 +287,18 @@ public class PostgresDbClient : IPostgresDbClient
string key = dataReader.GetString(dataReader.GetOrdinal("key"));
string metadata = dataReader.GetString(dataReader.GetOrdinal("metadata"));
Vector? embedding = withEmbeddings ? await dataReader.GetFieldValueAsync<Vector>(dataReader.GetOrdinal("embedding"), cancellationToken).ConfigureAwait(false) : null;
long? timestamp = await dataReader.GetFieldValueAsync<long?>(dataReader.GetOrdinal("timestamp"), cancellationToken).ConfigureAwait(false);
DateTime? timestamp = await dataReader.GetFieldValueAsync<DateTime?>(dataReader.GetOrdinal("timestamp"), cancellationToken).ConfigureAwait(false);
return new PostgresMemoryEntry() { Key = key, MetadataString = metadata, Embedding = embedding, Timestamp = timestamp };
}
/// <summary>
/// Create a collection as table.
/// Get full table name with schema from table name.
/// </summary>
/// <param name="connection">An opened <see cref="NpgsqlConnection"/> instance.</param>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="tableName"></param>
/// <returns></returns>
private async Task CreateTableAsync(NpgsqlConnection connection, string collectionName, CancellationToken cancellationToken = default)
private string GetFullTableName(string tableName)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $@"
CREATE TABLE IF NOT EXISTS {this.GetTableName(collectionName)} (
key TEXT NOT NULL,
metadata JSONB,
embedding vector({this._vectorSize}),
timestamp BIGINT,
PRIMARY KEY (key))";
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Create index for collection table.
/// </summary>
/// <param name="connection">An opened <see cref="NpgsqlConnection"/> instance.</param>
/// <param name="collectionName">The name assigned to a collection of entries.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns></returns>
private async Task CreateIndexAsync(NpgsqlConnection connection, string collectionName, CancellationToken cancellationToken = default)
{
using NpgsqlCommand cmd = connection.CreateCommand();
cmd.CommandText = $@"
CREATE INDEX IF NOT EXISTS {collectionName}_ix
ON {this.GetTableName(collectionName)} USING ivfflat (embedding vector_cosine_ops) WITH (lists = {this._numberOfLists})";
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get table name from collection name.
/// </summary>
/// <param name="collectionName"></param>
/// <returns></returns>
private string GetTableName(string collectionName)
{
return $"{this._schema}.\"{collectionName}\"";
return $"{this._schema}.\"{tableName}\"";
}
#endregion
}
@@ -18,18 +18,16 @@ public static class PostgresKernelBuilderExtensions
/// <param name="builder">The <see cref="KernelBuilder"/> instance</param>
/// <param name="dataSource">Postgres data source.</param>
/// <param name="vectorSize">Embedding vector size.</param>
/// <param name="schema">Schema of collection tables.</param>
/// <param name="numberOfLists">Specifies the number of lists for indexing. Higher values can improve recall but may impact performance. The default value is 1000. More info <see href="https://github.com/pgvector/pgvector#indexing"/></param>
/// <param name="schema">Schema of collection tables. The default value is "public".</param>
/// <returns>Self instance</returns>
public static KernelBuilder WithPostgresMemoryStore(this KernelBuilder builder,
NpgsqlDataSource dataSource,
int vectorSize,
string schema = PostgresMemoryStore.DefaultSchema,
int numberOfLists = PostgresMemoryStore.DefaultNumberOfLists)
string schema = PostgresMemoryStore.DefaultSchema)
{
builder.WithMemoryStorage((parameters) =>
{
return new PostgresMemoryStore(dataSource, vectorSize, schema, numberOfLists);
return new PostgresMemoryStore(dataSource, vectorSize, schema);
});
return builder;
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Pgvector;
namespace Microsoft.SemanticKernel.Connectors.Memory.Postgres;
@@ -25,7 +26,7 @@ public record struct PostgresMemoryEntry
public Vector? Embedding { get; set; }
/// <summary>
/// Optional timestamp.
/// Optional timestamp. Its 'DateTimeKind' is <see cref="DateTimeKind.Utc"/>
/// </summary>
public long? Timestamp { get; set; }
public DateTime? Timestamp { get; set; }
}
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -23,7 +22,6 @@ namespace Microsoft.SemanticKernel.Connectors.Memory.Postgres;
public class PostgresMemoryStore : IMemoryStore
{
internal const string DefaultSchema = "public";
internal const int DefaultNumberOfLists = 1000;
/// <summary>
/// Initializes a new instance of the <see cref="PostgresMemoryStore"/> class.
@@ -31,9 +29,8 @@ public class PostgresMemoryStore : IMemoryStore
/// <param name="dataSource">Postgres data source.</param>
/// <param name="vectorSize">Embedding vector size.</param>
/// <param name="schema">Database schema of collection tables. The default value is "public".</param>
/// <param name="numberOfLists">Specifies the number of lists for indexing. Higher values can improve recall but may impact performance. The default value is 1000. More info <see href="https://github.com/pgvector/pgvector#indexing"/></param>
public PostgresMemoryStore(NpgsqlDataSource dataSource, int vectorSize, string schema = DefaultSchema, int numberOfLists = DefaultNumberOfLists)
: this(new PostgresDbClient(dataSource, schema, vectorSize, numberOfLists))
public PostgresMemoryStore(NpgsqlDataSource dataSource, int vectorSize, string schema = DefaultSchema)
: this(new PostgresDbClient(dataSource, schema, vectorSize))
{
}
@@ -47,7 +44,7 @@ public class PostgresMemoryStore : IMemoryStore
{
Verify.NotNullOrWhiteSpace(collectionName);
await this._postgresDbClient.CreateCollectionAsync(collectionName, cancellationToken).ConfigureAwait(false);
await this._postgresDbClient.CreateTableAsync(collectionName, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -55,13 +52,13 @@ public class PostgresMemoryStore : IMemoryStore
{
Verify.NotNullOrWhiteSpace(collectionName);
return await this._postgresDbClient.DoesCollectionExistsAsync(collectionName, cancellationToken).ConfigureAwait(false);
return await this._postgresDbClient.DoesTableExistsAsync(collectionName, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async IAsyncEnumerable<string> GetCollectionsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var collection in this._postgresDbClient.GetCollectionsAsync(cancellationToken).ConfigureAwait(false))
await foreach (string collection in this._postgresDbClient.GetTablesAsync(cancellationToken).ConfigureAwait(false))
{
yield return collection;
}
@@ -72,7 +69,7 @@ public class PostgresMemoryStore : IMemoryStore
{
Verify.NotNullOrWhiteSpace(collectionName);
await this._postgresDbClient.DeleteCollectionAsync(collectionName, cancellationToken).ConfigureAwait(false);
await this._postgresDbClient.DeleteTableAsync(collectionName, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -89,7 +86,7 @@ public class PostgresMemoryStore : IMemoryStore
{
Verify.NotNullOrWhiteSpace(collectionName);
foreach (var record in records)
foreach (MemoryRecord record in records)
{
yield return await this.InternalUpsertAsync(collectionName, record, cancellationToken).ConfigureAwait(false);
}
@@ -100,7 +97,11 @@ public class PostgresMemoryStore : IMemoryStore
{
Verify.NotNullOrWhiteSpace(collectionName);
return await this.InternalGetAsync(collectionName, key, withEmbedding, cancellationToken).ConfigureAwait(false);
PostgresMemoryEntry? entry = await this._postgresDbClient.ReadAsync(collectionName, key, withEmbedding, cancellationToken).ConfigureAwait(false);
if (!entry.HasValue) { return null; }
return this.GetMemoryRecordFromEntry(entry.Value);
}
/// <inheritdoc/>
@@ -109,13 +110,9 @@ public class PostgresMemoryStore : IMemoryStore
{
Verify.NotNullOrWhiteSpace(collectionName);
foreach (var key in keys)
await foreach (PostgresMemoryEntry entry in this._postgresDbClient.ReadBatchAsync(collectionName, keys, withEmbeddings, cancellationToken).ConfigureAwait(false))
{
var result = await this.InternalGetAsync(collectionName, key, withEmbeddings, cancellationToken).ConfigureAwait(false);
if (result != null)
{
yield return result;
}
yield return this.GetMemoryRecordFromEntry(entry);
}
}
@@ -132,10 +129,7 @@ public class PostgresMemoryStore : IMemoryStore
{
Verify.NotNullOrWhiteSpace(collectionName);
foreach (var key in keys)
{
await this._postgresDbClient.DeleteAsync(collectionName, key, cancellationToken).ConfigureAwait(false);
}
await this._postgresDbClient.DeleteBatchAsync(collectionName, keys, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -155,21 +149,16 @@ public class PostgresMemoryStore : IMemoryStore
}
IAsyncEnumerable<(PostgresMemoryEntry, double)> results = this._postgresDbClient.GetNearestMatchesAsync(
collectionName: collectionName,
embeddingFilter: new Vector(embedding.Vector.ToArray()),
tableName: collectionName,
embedding: new Vector(embedding.Vector.ToArray()),
limit: limit,
minRelevanceScore: minRelevanceScore,
withEmbeddings: withEmbeddings,
cancellationToken: cancellationToken);
await foreach (var (entry, cosineSimilarity) in results.ConfigureAwait(false))
await foreach ((PostgresMemoryEntry entry, double cosineSimilarity) in results.ConfigureAwait(false))
{
MemoryRecord record = MemoryRecord.FromJsonMetadata(
json: entry.MetadataString,
this.GetEmbeddingForEntry(entry),
entry.Key,
ParseTimestamp(entry.Timestamp));
yield return (record, cosineSimilarity);
yield return (this.GetMemoryRecordFromEntry(entry), cosineSimilarity);
}
}
@@ -190,52 +179,28 @@ public class PostgresMemoryStore : IMemoryStore
private readonly IPostgresDbClient _postgresDbClient;
private static long? ToTimestampLong(DateTimeOffset? timestamp)
{
return timestamp?.ToUnixTimeMilliseconds();
}
private static DateTimeOffset? ParseTimestamp(long? timestamp)
{
if (timestamp.HasValue)
{
return DateTimeOffset.FromUnixTimeMilliseconds(timestamp.Value);
}
return null;
}
private async Task<string> InternalUpsertAsync(string collectionName, MemoryRecord record, CancellationToken cancellationToken)
{
record.Key = record.Metadata.Id;
await this._postgresDbClient.UpsertAsync(
collectionName: collectionName,
tableName: collectionName,
key: record.Key,
metadata: record.GetSerializedMetadata(),
embedding: new Vector(record.Embedding.Vector.ToArray()),
timestamp: ToTimestampLong(record.Timestamp),
timestamp: record.Timestamp?.UtcDateTime,
cancellationToken: cancellationToken).ConfigureAwait(false);
return record.Key;
}
private async Task<MemoryRecord?> InternalGetAsync(string collectionName, string key, bool withEmbedding, CancellationToken cancellationToken)
private MemoryRecord GetMemoryRecordFromEntry(PostgresMemoryEntry entry)
{
PostgresMemoryEntry? entry = await this._postgresDbClient.ReadAsync(collectionName, key, withEmbedding, cancellationToken).ConfigureAwait(false);
if (!entry.HasValue) { return null; }
return MemoryRecord.FromJsonMetadata(
json: entry.Value.MetadataString,
embedding: this.GetEmbeddingForEntry(entry.Value),
entry.Value.Key,
ParseTimestamp(entry.Value.Timestamp));
}
private Embedding<float> GetEmbeddingForEntry(PostgresMemoryEntry entry)
{
return entry.Embedding != null ? new Embedding<float>(entry.Embedding!.ToArray()) : Embedding<float>.Empty;
json: entry.MetadataString,
embedding: entry.Embedding != null ? new Embedding<float>(entry.Embedding!.ToArray()) : Embedding<float>.Empty,
key: entry.Key,
timestamp: entry.Timestamp?.ToLocalTime());
}
#endregion
@@ -35,20 +35,57 @@ sk_demo=# CREATE EXTENSION vector;
3. To use Postgres as a semantic memory store:
```csharp
NpgsqlDataSourceBuilder dataSourceBuilder = new NpgsqlDataSourceBuilder("Host=localhost;Port=5432;Database=sk_memory;User Id=postgres;Password=mysecretpassword");
NpgsqlDataSourceBuilder dataSourceBuilder = new NpgsqlDataSourceBuilder("Host=localhost;Port=5432;Database=sk_demo;User Id=postgres;Password=mysecretpassword");
dataSourceBuilder.UseVector();
NpgsqlDataSource dataSource = dataSourceBuilder.Build();
PostgresMemoryStore memoryStore = new PostgresMemoryStore(dataSource, vectorSize: 1536/*, schema: "public", numberOfLists: 1000 */);
PostgresMemoryStore memoryStore = new PostgresMemoryStore(dataSource, vectorSize: 1536/*, schema: "public" */);
IKernel kernel = Kernel.Builder
.WithLogger(ConsoleLogger.Log)
.WithOpenAITextEmbeddingGenerationService("text-embedding-ada-002", Env.Var("OPENAI_API_KEY"))
.WithMemoryStorage(memoryStore)
//.WithPostgresMemoryStore(dataSource, vectorSize: 1536, schema: "public", numberOfLists: 1000) // This method offers an alternative approach to registering Postgres memory store.
//.WithPostgresMemoryStore(dataSource, vectorSize: 1536, schema: "public") // This method offers an alternative approach to registering Postgres memory store.
.Build();
```
### Create Index
> By default, pgvector performs exact nearest neighbor search, which provides perfect recall.
> You can add an index to use approximate nearest neighbor search, which trades some recall for performance. Unlike typical indexes, you will see different results for queries after adding an approximate index.
> Three keys to achieving good recall are:
> - Create the index after the table has some data
> - Choose an appropriate number of lists - a good place to start is rows / 1000 for up to 1M rows and sqrt(rows) for over 1M rows
> - When querying, specify an appropriate number of probes (higher is better for recall, lower is better for speed) - a good place to start is sqrt(lists)
Please read [the documentation](https://github.com/pgvector/pgvector#indexing) for more information.
Based on the data rows of your collection table, consider the following statement to create an index.
```sql
DO $$
DECLARE
collection TEXT;
c_count INTEGER;
BEGIN
SELECT 'REPLACE YOUR COLLECTION TABLE NAME' INTO collection;
-- Get count of records in collection
EXECUTE format('SELECT count(*) FROM public.%I;', collection) INTO c_count;
-- Create Index (https://github.com/pgvector/pgvector#indexing)
IF c_count > 10000000 THEN
EXECUTE format('CREATE INDEX %I ON public.%I USING ivfflat (embedding vector_cosine_ops) WITH (lists = %s);',
collection || '_ix', collection, ROUND(sqrt(c_count)));
ELSIF c_count > 10000 THEN
EXECUTE format('CREATE INDEX %I ON public.%I USING ivfflat (embedding vector_cosine_ops) WITH (lists = %s);',
collection || '_ix', collection, c_count / 1000);
END IF;
END $$;
```
## Migration from older versions
Since Postgres Memory connector has been re-implemented, the new implementation uses a separate table to store each Collection.
@@ -64,6 +101,7 @@ We provide the following migration script to help you migrate to the new structu
DO $$
DECLARE
r record;
c_count integer;
BEGIN
FOR r IN SELECT DISTINCT collection FROM sk_memory_table LOOP
@@ -75,14 +113,23 @@ BEGIN
key TEXT NOT NULL,
metadata JSONB,
embedding vector(1536),
timestamp BIGINT,
timestamp TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (key)
);', r.collection);
-- Create Index (You can modify the size of lists according to your data needs. Its default value is 1000.)
EXECUTE format('CREATE INDEX %I
ON public.%I USING ivfflat (embedding vector_cosine_ops) WITH (lists = 1000);',
r.collection || '_ix', r.collection);
-- Get count of records in collection
SELECT count(*) INTO c_count FROM sk_memory_table WHERE collection = r.collection AND key <> '';
-- Create Index (https://github.com/pgvector/pgvector#indexing)
IF c_count > 10000000 THEN
EXECUTE format('CREATE INDEX %I
ON public.%I USING ivfflat (embedding vector_cosine_ops) WITH (lists = %s);',
r.collection || '_ix', r.collection, ROUND(sqrt(c_count)));
ELSIF c_count > 10000 THEN
EXECUTE format('CREATE INDEX %I
ON public.%I USING ivfflat (embedding vector_cosine_ops) WITH (lists = %s);',
r.collection || '_ix', r.collection, c_count / 1000);
END IF;
END LOOP;
END $$;
@@ -93,7 +140,7 @@ DECLARE
BEGIN
FOR r IN SELECT DISTINCT collection FROM sk_memory_table LOOP
EXECUTE format('INSERT INTO public.%I (key, metadata, embedding, timestamp)
SELECT key, metadata::JSONB, embedding, timestamp
SELECT key, metadata::JSONB, embedding, to_timestamp(timestamp / 1000.0) AT TIME ZONE ''UTC''
FROM sk_memory_table WHERE collection = %L AND key <> '''';', r.collection, r.collection);
END LOOP;
END $$;
@@ -1,13 +1,17 @@
# Microsoft.SemanticKernel.Connectors.Memory.Redis
This connector uses Redis to implement Semantic Memory. It requires the [RediSearch](https://redis.io/docs/stack/search) module to be enabled on Redis to implement vector similarity search.
This connector uses Redis to implement Semantic Memory. It requires the [RediSearch](https://redis.io/docs/interact/search-and-query/) module to be enabled on Redis to implement vector similarity search.
## What is RediSearch?
[RediSearch](https://redis.io/docs/stack/search) is a source-available Redis module that enables querying, secondary indexing, and full-text search for Redis. These features enable multi-field queries, aggregation, exact phrase matching, numeric filtering, geo filtering and vector similarity semantic search on top of text queries.
[RediSearch](https://redis.io/docs/interact/search-and-query/) is a source-available Redis module that enables querying, secondary indexing, and full-text search for Redis. These features enable multi-field queries, aggregation, exact phrase matching, numeric filtering, geo filtering and vector similarity semantic search on top of text queries.
How to set up the RediSearch, please refer to its [documentation](https://redis.io/docs/stack/search/quick_start/).
Or use the [Redis Enterprise](https://redis.io/docs/about/redis-enterprise/), see [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/garantiadata.redis_enterprise_1sp_public_preview?tab=Overview), [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-e6y7ork67pjwg?sr=0-2&ref_=beagle&applicationId=AWSMPContessa), or [Google Marketplace](https://console.cloud.google.com/marketplace/details/redislabs-public/redis-enterprise?pli=1).
Ways to get RediSearch:
1. You can create an [Azure Cache for Redis Enterpise instance](https://learn.microsoft.com/azure/azure-cache-for-redis/quickstart-create-redis-enterprise) and [enable RediSearch module](https://learn.microsoft.com/azure/azure-cache-for-redis/cache-redis-modules).
1. Set up the RediSearch on your self-managed Redis, please refer to its [documentation](https://redis.io/docs/interact/search-and-query/quickstart/).
1. Use the [Redis Enterprise](https://redis.io/docs/about/redis-enterprise/), see [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/garantiadata.redis_enterprise_1sp_public_preview?tab=Overview), [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-e6y7ork67pjwg?sr=0-2&ref_=beagle&applicationId=AWSMPContessa), or [Google Marketplace](https://console.cloud.google.com/marketplace/details/redislabs-public/redis-enterprise?pli=1).
## Quick start
@@ -20,6 +20,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- Because some of the referenced projects have dependencies that themselves have System.Text.Json set with a minimum of 7.0. -->
<PackageReference Include="System.Text.Json" />
<PackageVersion Update="System.Text.Json" Version="7.0.3" />
</ItemGroup>
<Import Project="$(RepoRoot)/dotnet/src/InternalUtilities/src/InternalUtilities.props" />
@@ -26,7 +26,7 @@ public class PostgresMemoryStoreTests
{
this._postgresDbClientMock = new Mock<IPostgresDbClient>();
this._postgresDbClientMock
.Setup(client => client.DoesCollectionExistsAsync(CollectionName, CancellationToken.None))
.Setup(client => client.DoesTableExistsAsync(CollectionName, CancellationToken.None))
.ReturnsAsync(true);
}
@@ -40,7 +40,7 @@ public class PostgresMemoryStoreTests
await store.CreateCollectionAsync(CollectionName);
// Assert
this._postgresDbClientMock.Verify(client => client.CreateCollectionAsync(CollectionName, CancellationToken.None), Times.Once());
this._postgresDbClientMock.Verify(client => client.CreateTableAsync(CollectionName, CancellationToken.None), Times.Once());
}
[Fact]
@@ -53,7 +53,7 @@ public class PostgresMemoryStoreTests
await store.DeleteCollectionAsync(CollectionName);
// Assert
this._postgresDbClientMock.Verify(client => client.DeleteCollectionAsync(CollectionName, CancellationToken.None), Times.Once());
this._postgresDbClientMock.Verify(client => client.DeleteTableAsync(CollectionName, CancellationToken.None), Times.Once());
}
[Fact]
@@ -76,7 +76,7 @@ public class PostgresMemoryStoreTests
const string collectionName = "non-existent-collection";
this._postgresDbClientMock
.Setup(client => client.DoesCollectionExistsAsync(collectionName, CancellationToken.None))
.Setup(client => client.DoesTableExistsAsync(collectionName, CancellationToken.None))
.ReturnsAsync(false);
var store = new PostgresMemoryStore(this._postgresDbClientMock.Object);
@@ -192,12 +192,13 @@ public class PostgresMemoryStoreTests
.ReturnsAsync(this.GetPostgresMemoryEntryFromMemoryRecord(memoryRecord));
}
var doesNotExistMemoryKey = "fake-record-key";
this._postgresDbClientMock
.Setup(client => client.ReadAsync(CollectionName, doesNotExistMemoryKey, true, CancellationToken.None))
.ReturnsAsync((PostgresMemoryEntry?)null);
memoryRecordKeys.Insert(0, "non-existent-record-key-1");
memoryRecordKeys.Insert(2, "non-existent-record-key-2");
memoryRecordKeys.Add("non-existent-record-key-3");
memoryRecordKeys.Add(doesNotExistMemoryKey);
this._postgresDbClientMock
.Setup(client => client.ReadBatchAsync(CollectionName, memoryRecordKeys, true, CancellationToken.None))
.Returns(expectedMemoryRecords.Select(memoryRecord => this.GetPostgresMemoryEntryFromMemoryRecord(memoryRecord)).ToAsyncEnumerable());
var store = new PostgresMemoryStore(this._postgresDbClientMock.Object);
@@ -205,6 +206,7 @@ public class PostgresMemoryStoreTests
var actualMemoryRecords = await store.GetBatchAsync(CollectionName, memoryRecordKeys, withEmbeddings: true).ToListAsync();
// Assert
this._postgresDbClientMock.Verify(client => client.ReadBatchAsync(CollectionName, memoryRecordKeys, true, CancellationToken.None), Times.Once());
Assert.Equal(expectedMemoryRecords.Length, actualMemoryRecords.Count);
for (var i = 0; i < expectedMemoryRecords.Length; i++)
@@ -220,7 +222,7 @@ public class PostgresMemoryStoreTests
var expectedCollections = new List<string> { "fake-collection-1", "fake-collection-2", "fake-collection-3" };
this._postgresDbClientMock
.Setup(client => client.GetCollectionsAsync(CancellationToken.None))
.Setup(client => client.GetTablesAsync(CancellationToken.None))
.Returns(expectedCollections.ToAsyncEnumerable());
var store = new PostgresMemoryStore(this._postgresDbClientMock.Object);
@@ -262,10 +264,7 @@ public class PostgresMemoryStoreTests
await store.RemoveBatchAsync(CollectionName, memoryRecordKeys);
// Assert
foreach (var memoryRecordKey in memoryRecordKeys)
{
this._postgresDbClientMock.Verify(client => client.DeleteAsync(CollectionName, memoryRecordKey, CancellationToken.None), Times.Once());
}
this._postgresDbClientMock.Verify(client => client.DeleteBatchAsync(CollectionName, memoryRecordKeys, CancellationToken.None), Times.Once());
}
#region private ================================================================================
@@ -293,7 +292,8 @@ public class PostgresMemoryStoreTests
description: "description-" + Guid.NewGuid().ToString(),
embedding: memoryEmbedding,
additionalMetadata: "metadata-" + Guid.NewGuid().ToString(),
key: id);
key: id,
timestamp: DateTimeOffset.Now);
}
private PostgresMemoryEntry GetPostgresMemoryEntryFromMemoryRecord(MemoryRecord memoryRecord)
@@ -303,7 +303,7 @@ public class PostgresMemoryStoreTests
Key = memoryRecord.Key,
Embedding = new Pgvector.Vector(memoryRecord.Embedding.Vector.ToArray()),
MetadataString = memoryRecord.GetSerializedMetadata(),
Timestamp = memoryRecord.Timestamp?.ToUnixTimeMilliseconds()
Timestamp = memoryRecord.Timestamp?.UtcDateTime
};
}
@@ -0,0 +1,161 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.SkillDefinition;
using Moq;
using Xunit;
namespace SemanticKernel.Extensions.UnitTests.Planning.ActionPlanner;
public sealed class ActionPlannerTests
{
[Fact]
public async Task ExtractsAndDeserializesWellFormedJsonFromPlannerResult()
{
// Arrange
var functions = new List<(string name, string skillName, string description, bool isSemantic)>()
{
("SendEmail", "email", "Send an e-mail", false),
("PullsList", "GitHubSkill", "List pull requests", true)
};
var functionsView = new FunctionsView();
var skills = new Mock<ISkillCollection>();
foreach (var (name, skillName, description, isSemantic) in functions)
{
var functionView = new FunctionView(name, skillName, description, new List<ParameterView>(), isSemantic, true);
var mockFunction = CreateMockFunction(functionView);
functionsView.AddFunction(functionView);
mockFunction.Setup(x =>
x.InvokeAsync(It.IsAny<SKContext>(), It.IsAny<CompleteRequestSettings>()))
.Returns<SKContext, CompleteRequestSettings>((context, settings) =>
{
context.Variables.Update("MOCK FUNCTION CALLED");
return Task.FromResult(context);
});
skills.Setup(x => x.GetFunction(It.Is<string>(s => s == skillName), It.Is<string>(s => s == name)))
.Returns(mockFunction.Object);
ISKFunction? outFunc = mockFunction.Object;
skills.Setup(x => x.TryGetFunction(It.Is<string>(s => s == skillName), It.Is<string>(s => s == name), out outFunc)).Returns(true);
}
skills.Setup(x => x.GetFunctionsView(It.IsAny<bool>(), It.IsAny<bool>())).Returns(functionsView);
string planString = "Here is a possible plan to accomplish the user intent:\n\n{\"plan\":{\n\"rationale\": \"the list contains a function that allows to list pull requests\",\n\"function\": \"GitHubSkill.PullsList\",\n\"parameters\": {\n\"owner\": \"microsoft\",\n\"repo\": \"semantic-kernel\",\n\"state\": \"open\"\n}}}\n\nThis plan uses the `GitHubSkill.PullsList` function to list the open pull requests for the `semantic-kernel` repository owned by `microsoft`. The `state` parameter is set to `\"open\"` to filter the results to only show open pull requests.";
var kernel = this.CreateMockKernelAndFunctionFlowWithTestString(planString, skills);
var planner = new Microsoft.SemanticKernel.Planning.ActionPlanner(kernel.Object);
// Act
var plan = await planner.CreatePlanAsync("goal");
// Assert
Assert.Equal("goal", plan.Description);
Assert.Equal(plan.Steps.Count, 1);
Assert.Equal(plan.Steps[0].SkillName, "GitHubSkill");
Assert.Equal(plan.Steps[0].Name, "PullsList");
}
[Fact]
public async Task InvalidJsonThrowsAsync()
{
// Arrange
string invalidJsonString = "<>";
var kernel = this.CreateMockKernelAndFunctionFlowWithTestString(invalidJsonString);
var planner = new Microsoft.SemanticKernel.Planning.ActionPlanner(kernel.Object);
// Act & Assert
await Assert.ThrowsAsync<PlanningException>(async () => await planner.CreatePlanAsync("goal"));
}
[Fact]
public async Task MalformedJsonThrowsAsync()
{
// Arrange
// Extra opening brace before rationale
string invalidJsonString = "Here is a possible plan to accomplish the user intent:\n\n{\"plan\": { {\n\"rationale\": \"the list contains a function that allows to list pull requests\",\n\"function\": \"GitHubSkill.PullsList\",\n\"parameters\": {\n\"owner\": \"microsoft\",\n\"repo\": \"semantic-kernel\",\n\"state\": \"open\"\n}}}\n\nThis plan uses the `GitHubSkill.PullsList` function to list the open pull requests for the `semantic-kernel` repository owned by `microsoft`. The `state` parameter is set to `\"open\"` to filter the results to only show open pull requests.";
var kernel = this.CreateMockKernelAndFunctionFlowWithTestString(invalidJsonString);
var planner = new Microsoft.SemanticKernel.Planning.ActionPlanner(kernel.Object);
// Act & Assert
await Assert.ThrowsAsync<PlanningException>(async () => await planner.CreatePlanAsync("goal"));
}
private Mock<IKernel> CreateMockKernelAndFunctionFlowWithTestString(string testPlanString, Mock<ISkillCollection>? mockSkills = null)
{
var kernel = new Mock<IKernel>();
var memory = new Mock<ISemanticTextMemory>();
var skills = mockSkills;
if (mockSkills == null)
{
skills = new Mock<ISkillCollection>();
var functionsView = new FunctionsView();
skills.Setup(x => x.GetFunctionsView(It.IsAny<bool>(), It.IsAny<bool>())).Returns(functionsView);
}
var returnContext = new SKContext(
new ContextVariables(testPlanString),
memory.Object,
skills!.Object,
new Mock<ILogger>().Object
);
var context = new SKContext(
new ContextVariables(),
memory.Object,
skills!.Object,
new Mock<ILogger>().Object
);
var mockFunctionFlowFunction = new Mock<ISKFunction>();
mockFunctionFlowFunction.Setup(x => x.InvokeAsync(
It.IsAny<SKContext>(),
null
)).Callback<SKContext, CompleteRequestSettings>(
(c, s) => c.Variables.Update("Hello world!")
).Returns(() => Task.FromResult(returnContext));
// Mock Skills
kernel.Setup(x => x.Skills).Returns(skills.Object);
kernel.Setup(x => x.CreateNewContext(It.IsAny<CancellationToken>())).Returns(context);
kernel.Setup(x => x.RegisterSemanticFunction(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<SemanticFunctionConfig>()
)).Returns(mockFunctionFlowFunction.Object);
return kernel;
}
// Method to create Mock<ISKFunction> objects
private static Mock<ISKFunction> CreateMockFunction(FunctionView functionView)
{
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.Describe()).Returns(functionView);
mockFunction.Setup(x => x.Name).Returns(functionView.Name);
mockFunction.Setup(x => x.SkillName).Returns(functionView.SkillName);
return mockFunction;
}
}
@@ -8,7 +8,6 @@ using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.Planning.Sequential;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.SkillDefinition;
using Moq;
@@ -86,8 +85,7 @@ public class SequentialPlanParserTests
kernelMock.Setup(x => x.RegisterSemanticFunction(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<SemanticFunctionConfig>(),
It.IsAny<ITrustService?>()
It.IsAny<SemanticFunctionConfig>()
)).Returns(mockFunction.Object);
}
else
@@ -10,7 +10,6 @@ using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.SkillDefinition;
using Moq;
@@ -104,8 +103,7 @@ public sealed class SequentialPlannerTests
kernel.Setup(x => x.RegisterSemanticFunction(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<SemanticFunctionConfig>(),
It.IsAny<ITrustService?>()
It.IsAny<SemanticFunctionConfig>()
)).Returns(mockFunctionFlowFunction.Object);
var planner = new Microsoft.SemanticKernel.Planning.SequentialPlanner(kernel.Object);
@@ -192,8 +190,7 @@ public sealed class SequentialPlannerTests
kernel.Setup(x => x.RegisterSemanticFunction(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<SemanticFunctionConfig>(),
It.IsAny<ITrustService?>()
It.IsAny<SemanticFunctionConfig>()
)).Returns(mockFunctionFlowFunction.Object);
var planner = new Microsoft.SemanticKernel.Planning.SequentialPlanner(kernel.Object);
@@ -6,6 +6,7 @@ using System.ComponentModel;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -81,23 +82,7 @@ public sealed class ActionPlanner
this._context.Variables.Update(goal);
SKContext result = await this._plannerFunction.InvokeAsync(this._context).ConfigureAwait(false);
ActionPlanResponse? planData;
try
{
planData = JsonSerializer.Deserialize<ActionPlanResponse?>(result.ToString(), new JsonSerializerOptions
{
AllowTrailingCommas = true,
DictionaryKeyPolicy = null,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
PropertyNameCaseInsensitive = true,
});
}
catch (Exception e)
{
throw new PlanningException(PlanningException.ErrorCodes.InvalidPlan,
"Plan parsing error, invalid JSON", e);
}
ActionPlanResponse? planData = this.ParsePlannerResult(result);
if (planData == null)
{
@@ -226,6 +211,44 @@ Goal: tell me a joke.
";
}
#region private ================================================================================
/// <summary>
/// Native function that filters out good JSON from planner result in case additional text is present
/// using a similar regex to the balancing group regex defined here: https://learn.microsoft.com/en-us/dotnet/standard/base-types/grouping-constructs-in-regular-expressions#balancing-group-definitions
/// </summary>
/// <param name="plannerResult">Result context of planner function.</param>
/// <returns>Instance of <see cref="ActionPlanResponse"/> object deserialized from extracted JSON.</returns>
private ActionPlanResponse? ParsePlannerResult(SKContext plannerResult)
{
Regex planRegex = new("^[^{}]*(((?'Open'{)[^{}]*)+((?'Close-Open'})[^{}]*)+)*(?(Open)(?!))", RegexOptions.Singleline);
Match match = planRegex.Match(plannerResult.ToString());
if (match.Success && match.Groups["Close"].Length > 0)
{
string planJson = $"{{{match.Groups["Close"]}}}";
try
{
return JsonSerializer.Deserialize<ActionPlanResponse?>(planJson, new JsonSerializerOptions
{
AllowTrailingCommas = true,
DictionaryKeyPolicy = null,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
PropertyNameCaseInsensitive = true,
});
}
catch (Exception e)
{
throw new PlanningException(PlanningException.ErrorCodes.InvalidPlan,
"Plan parsing error, invalid JSON", e);
}
}
else
{
throw new PlanningException(PlanningException.ErrorCodes.InvalidPlan, $"Failed to extract valid json string from planner result: '{plannerResult}'");
}
}
private void PopulateList(StringBuilder list, IDictionary<string, List<FunctionView>> functions)
{
foreach (KeyValuePair<string, List<FunctionView>> skill in functions)
@@ -264,4 +287,6 @@ Goal: tell me a joke.
{
return x.EndsWith(".", StringComparison.Ordinal) ? x : $"{x}.";
}
#endregion
}
@@ -52,7 +52,7 @@ public sealed class SequentialPlannerConfig
/// <summary>
/// The maximum number of tokens to allow in a plan.
/// </summary>
public int MaxTokens { get; set; } = 1024;
public int? MaxTokens { get; set; }
/// <summary>
/// Whether to allow missing functions in the plan on creation.
@@ -27,6 +27,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- Because some of the referenced projects have dependencies that themselves have System.Text.Json set with a minimum of 7.0. -->
<PackageReference Include="System.Text.Json" />
<PackageVersion Update="System.Text.Json" Version="7.0.3" />
</ItemGroup>
<ItemGroup>
@@ -146,7 +146,6 @@ public sealed class PlanTests : IDisposable
}
[Theory]
[InlineData(null, "Write a poem or joke and send it in an e-mail to Kai.", null)]
[InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")]
[InlineData("Hello World!", "Write a poem or joke and send it in an e-mail to Kai.", "[email protected]")]
public async Task CanExecuteRunPlanSimpleManualStateAsync(string input, string goal, string email)
@@ -180,7 +179,6 @@ public sealed class PlanTests : IDisposable
}
[Theory]
[InlineData(null, "Write a poem or joke and send it in an e-mail to Kai.", null)]
[InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")]
[InlineData("Hello World!", "Write a poem or joke and send it in an e-mail to Kai.", "[email protected]")]
public async Task CanExecuteRunPlanSimpleManualStateNoVariableAsync(string input, string goal, string email)
@@ -214,14 +212,12 @@ public sealed class PlanTests : IDisposable
}
[Theory]
[InlineData(null, "Write a poem or joke and send it in an e-mail to Kai.", null)]
[InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")]
[InlineData("Hello World!", "Write a poem or joke and send it in an e-mail to Kai.", "[email protected]")]
public async Task CanExecuteRunPlanManualStateAsync(string input, string goal, string email)
{
// Arrange
IKernel target = this.InitializeKernel();
var emailSkill = target.ImportSkill(new EmailSkillFake());
// Create the input mapping from parent (plan) plan state to child plan (sendEmailPlan) state.
@@ -27,9 +27,9 @@ public class RedirectOutput : TextWriter, ILogger
this._logs.AppendLine(value);
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
public IDisposable BeginScope<TState>(TState state)
{
return null;
return null!;
}
public bool IsEnabled(LogLevel logLevel)
@@ -1,420 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SkillDefinition;
using SemanticKernel.IntegrationTests.TestSettings;
using Xunit;
using Xunit.Abstractions;
namespace SemanticKernel.IntegrationTests.Security;
public sealed class TrustServiceTests
{
public TrustServiceTests(ITestOutputHelper output)
{
this._configuration = new ConfigurationBuilder()
.AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddUserSecrets<TrustServiceTests>()
.Build();
}
[Fact]
public async void FlowWithUntrustedContentAndNotSensitiveSemanticFunctionShouldSucceed()
{
// Arrange
var valueToEcho = "Hello AI :)";
var kernel = this.InitializeKernel();
var defaultUntrustedEchoFunction = this.CreateSemanticEchoFunction(
kernel,
functionName: "DefaultUntrustedEchoFunction",
isSensitive: false,
trustService: TrustService.DefaultUntrusted
);
var defaultTrustedEchoFunction = this.CreateSemanticEchoFunction(
kernel,
functionName: "DefaultTrustedEchoFunction",
isSensitive: false,
trustService: TrustService.DefaultTrusted
);
// Act
var result = await kernel.RunAsync(valueToEcho, defaultUntrustedEchoFunction, defaultTrustedEchoFunction);
// Assert
Assert.Equal(valueToEcho, result.Result);
Assert.False(result.IsTrusted);
}
[Fact]
public async void FlowWithUntrustedContentAndSensitiveSemanticFunctionShouldThrow()
{
// Arrange
var valueToEcho = "Hello AI :)";
var kernel = this.InitializeKernel();
var notSensitiveEchoFunction = this.CreateSemanticEchoFunction(
kernel,
functionName: "NotSensitiveEchoFunction",
isSensitive: false,
trustService: TrustService.DefaultUntrusted
);
var sensitiveEchoFunction = this.CreateSemanticEchoFunction(
kernel,
functionName: "SensitiveEchoFunction",
isSensitive: true,
trustService: TrustService.DefaultTrusted
);
// Act
var result = await kernel.RunAsync(valueToEcho, notSensitiveEchoFunction, sensitiveEchoFunction);
// Assert
this.AssertResultHasThrown(result);
}
[Fact]
public async void FlowWithUntrustedContentAndNotSensitiveNativeFunctionShouldSucceed()
{
// Arrange
var valueToEcho = "Hello AI :)";
var kernel = this.InitializeKernel();
var defaultUntrustedEchoFunction = this.CreateNativeEchoFunction(
kernel,
isSensitive: false,
trustService: TrustService.DefaultUntrusted
);
var defaultTrustedEchoFunction = this.CreateNativeEchoFunction(
kernel,
isSensitive: false,
trustService: TrustService.DefaultTrusted
);
// Act
var result = await kernel.RunAsync(valueToEcho, defaultUntrustedEchoFunction, defaultTrustedEchoFunction);
// Assert
Assert.Equal(valueToEcho, result.Result);
Assert.False(result.IsTrusted);
}
[Fact]
public async void FlowWithUntrustedContentAndSensitiveNativeFunctionShouldThrow()
{
// Arrange
var valueToEcho = "Hello AI :)";
var kernel = this.InitializeKernel();
var notSensitiveEchoFunction = this.CreateNativeEchoFunction(
kernel,
isSensitive: false,
trustService: TrustService.DefaultUntrusted
);
var sensitiveEchoFunction = this.CreateNativeEchoFunction(
kernel,
isSensitive: true,
trustService: TrustService.DefaultTrusted
);
// Act
var result = await kernel.RunAsync(valueToEcho, notSensitiveEchoFunction, sensitiveEchoFunction);
// Assert
this.AssertResultHasThrown(result);
}
[Fact]
public async void UntrustedVariableAndNotSensitiveSemanticFunctionShouldSucceed()
{
// Arrange
var valueToEcho = "Hello AI, ";
var extraVar = "welcome!";
var variables = new ContextVariables(valueToEcho);
var kernel = this.InitializeKernel();
var notSensitiveEchoFunction = this.CreateSemanticEchoFunction(
kernel,
functionName: "NotSensitiveEchoFunction",
isSensitive: false,
trustService: null // Use defaults
);
// Make this untrusted
variables.Set(TrustServiceTests.ExtraVarName, TrustAwareString.CreateUntrusted(extraVar));
// Act
var result = await kernel.RunAsync(variables, notSensitiveEchoFunction);
// Assert
Assert.Equal(valueToEcho + extraVar, result.Result);
Assert.False(result.IsTrusted);
}
[Fact]
public async void UntrustedVariableAndSensitiveSemanticFunctionShouldThrow()
{
// Arrange
var valueToEcho = "Hello AI, ";
var extraVar = "welcome!";
var variables = new ContextVariables(valueToEcho);
var kernel = this.InitializeKernel();
var sensitiveEchoFunction = this.CreateSemanticEchoFunction(
kernel,
functionName: "SensitiveEchoFunction",
isSensitive: true,
trustService: null // Use defaults
);
// Make this untrusted
variables.Set(TrustServiceTests.ExtraVarName, TrustAwareString.CreateUntrusted(extraVar));
// Act
var result = await kernel.RunAsync(variables, sensitiveEchoFunction);
// Assert
this.AssertResultHasThrown(result);
}
[Fact]
public async void UntrustedVariableAndNotSensitiveNativeFunctionShouldSucceed()
{
// Arrange
var valueToEcho = "Hello AI, ";
var extraVar = "welcome!";
var variables = new ContextVariables(valueToEcho);
var kernel = this.InitializeKernel();
var notSensitiveEchoFunction = this.CreateNativeEchoFunction(
kernel,
isSensitive: false,
trustService: null // Use defaults
);
// Make this untrusted
variables.Set(TrustServiceTests.ExtraVarName, TrustAwareString.CreateUntrusted(extraVar));
// Act
var result = await kernel.RunAsync(variables, notSensitiveEchoFunction);
// Assert
Assert.Equal(valueToEcho + extraVar, result.Result);
Assert.False(result.IsTrusted);
}
[Fact]
public async void UntrustedVariableAndSensitiveNativeFunctionShouldThrow()
{
// Arrange
var valueToEcho = "Hello AI, ";
var extraVar = "welcome!";
var variables = new ContextVariables(valueToEcho);
var kernel = this.InitializeKernel();
var sensitiveEchoFunction = this.CreateNativeEchoFunction(
kernel,
isSensitive: true,
trustService: null // Use defaults
);
// Make this untrusted
variables.Set(TrustServiceTests.ExtraVarName, TrustAwareString.CreateUntrusted(extraVar));
// Act
var result = await kernel.RunAsync(variables, sensitiveEchoFunction);
// Assert
this.AssertResultHasThrown(result);
}
[Fact]
public async void NotSensitiveFunctionWithTrustedFunctionCallShouldSucceed()
{
// Arrange
var valueToEcho = "Hello AI :)";
var kernel = this.InitializeKernel();
this.CreateNativeEchoFunction(
kernel,
isSensitive: true,
trustService: TrustService.DefaultTrusted
);
var sensitiveEchoWithFunctionCallFunction = this.CreateSemanticEchoWithFunctionCallFunction(
kernel,
functionName: "SensitiveEchoWithFunctionCallFunction",
isSensitive: true,
trustService: null // Use defaults
);
// Act
var result = await kernel.RunAsync(valueToEcho, sensitiveEchoWithFunctionCallFunction);
// Assert
Assert.Equal(valueToEcho, result.Result);
Assert.True(result.IsTrusted);
}
[Fact]
public async void SensitiveFunctionWithUntrustedFunctionCallShouldThrow()
{
// Arrange
var valueToEcho = "Hello AI :)";
var kernel = this.InitializeKernel();
this.CreateNativeEchoFunction(
kernel,
isSensitive: true,
trustService: TrustService.DefaultUntrusted // Forces output to be untrusted
);
var sensitiveEchoWithFunctionCallFunction = this.CreateSemanticEchoWithFunctionCallFunction(
kernel,
functionName: "SensitiveEchoWithFunctionCallFunction",
isSensitive: true,
trustService: null // Use defaults
);
// Act
var result = await kernel.RunAsync(valueToEcho, sensitiveEchoWithFunctionCallFunction);
// Assert
this.AssertResultHasThrown(result);
}
private IKernel InitializeKernel()
{
AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get<AzureOpenAIConfiguration>();
Assert.NotNull(azureOpenAIConfiguration);
var builder = Kernel.Builder
.WithAzureTextCompletionService(
deploymentName: azureOpenAIConfiguration.DeploymentName,
endpoint: azureOpenAIConfiguration.Endpoint,
apiKey: azureOpenAIConfiguration.ApiKey,
serviceId: azureOpenAIConfiguration.ServiceId,
setAsDefault: true
);
return builder.Build();
}
private ISKFunction CreateSemanticEchoFunction(
IKernel kernel,
string functionName,
bool isSensitive,
ITrustService? trustService)
{
string promptTemplate = TrustServiceTests.SemanticEchoPrompt.Replace(
TrustServiceTests.WhatToEcho,
$"{{{{$input}}}}{{{{${TrustServiceTests.ExtraVarName}}}}}",
System.StringComparison.Ordinal
);
return kernel.CreateSemanticFunction(
promptTemplate: promptTemplate,
functionName: functionName,
skillName: "CustomSkill",
isSensitive: isSensitive,
trustService: trustService
);
}
private ISKFunction CreateSemanticEchoWithFunctionCallFunction(
IKernel kernel,
string functionName,
bool isSensitive,
ITrustService? trustService)
{
string promptTemplate = TrustServiceTests.SemanticEchoPrompt.Replace(
TrustServiceTests.WhatToEcho,
$"{{{{{nameof(EchoSkill)}.{nameof(EchoSkill.NotSensitiveEcho)} $input}}}}",
System.StringComparison.Ordinal
);
return kernel.CreateSemanticFunction(
promptTemplate: promptTemplate,
functionName: functionName,
skillName: "CustomSkill",
isSensitive: isSensitive,
trustService: trustService
);
}
private ISKFunction CreateNativeEchoFunction(
IKernel kernel,
bool isSensitive,
ITrustService? trustService)
{
var skill = kernel.ImportSkill(
new EchoSkill(),
nameof(EchoSkill),
trustService: trustService
);
return isSensitive ?
skill[nameof(EchoSkill.SensitiveEcho)] :
skill[nameof(EchoSkill.NotSensitiveEcho)];
}
private void AssertResultHasThrown(SKContext result)
{
Assert.True(result.ErrorOccurred);
Assert.IsType<UntrustedContentException>(result.LastException);
Assert.Equal(
UntrustedContentException.ErrorCodes.SensitiveFunctionWithUntrustedContent,
((UntrustedContentException)result.LastException).ErrorCode
);
}
private readonly IConfigurationRoot _configuration;
private sealed class EchoSkill
{
[SKFunction(isSensitive: false), Description("Echoes a given text")]
public string NotSensitiveEcho(string input, string? extraVar = null) => input + extraVar;
[SKFunction(isSensitive: true), Description("Echoes a given text")]
public string SensitiveEcho(string input, string? extraVar = null) => input + extraVar;
}
private const string WhatToEcho = "[WHAT_TO_ECHO]";
private const string ExtraVarName = "extraVar";
private const string SemanticEchoPrompt = @$"Your goal is just to echo whatever input is given to you.
[EXAMPLES]
INPUT
-------------------
Hello, how are you?
RESULT
-------------------
Hello, how are you?
INPUT
-------------------
Hi!
RESULT
-------------------
Hi!
[END EXAMPLES]
INPUT
-------------------
{TrustServiceTests.WhatToEcho}
RESULT
-------------------
";
}
+1 -1
View File
@@ -28,7 +28,7 @@ internal sealed class XunitLogger<T> : ILogger<T>, IDisposable
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc/>
public IDisposable BeginScope<TState>(TState state) where TState : notnull
public IDisposable BeginScope<TState>(TState state)
=> this;
/// <inheritdoc/>
@@ -52,7 +52,7 @@ public class ChatRequestSettings
/// <summary>
/// The maximum number of tokens to generate in the completion.
/// </summary>
public int MaxTokens { get; set; } = 256;
public int? MaxTokens { get; set; }
/// <summary>
/// Modify the likelihood of specified tokens appearing in the completion.
@@ -40,7 +40,7 @@ public class CompleteRequestSettings
/// <summary>
/// The maximum number of tokens to generate in the completion.
/// </summary>
public int MaxTokens { get; set; } = 256;
public int? MaxTokens { get; set; }
/// <summary>
/// Sequences where the completion will stop generating further tokens.
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.Services;
using Microsoft.SemanticKernel.SkillDefinition;
@@ -44,24 +43,15 @@ public interface IKernel
/// </summary>
IReadOnlySkillCollection Skills { get; }
/// <summary>
/// Default service for trust check events in case a specific one is not provided at function creation.
/// Functions directly created through the kernel will use this trust service if no other is provided.
/// If null, the created functions will rely on the TrustService.DefaultTrusted implementation.
/// </summary>
ITrustService? TrustServiceInstance { get; }
/// <summary>
/// Build and register a function in the internal skill collection, in a global generic skill.
/// </summary>
/// <param name="functionName">Name of the semantic function. The name can contain only alphanumeric chars + underscore.</param>
/// <param name="functionConfig">Function configuration, e.g. I/O params, AI settings, localization details, etc.</param>
/// <param name="trustService">Service used for trust checks (if null will use the default registered in the kernel).</param>
/// <returns>A C# function wrapping AI logic, usually defined with natural language</returns>
ISKFunction RegisterSemanticFunction(
string functionName,
SemanticFunctionConfig functionConfig,
ITrustService? trustService = null);
SemanticFunctionConfig functionConfig);
/// <summary>
/// Build and register a function in the internal skill collection.
@@ -69,13 +59,11 @@ public interface IKernel
/// <param name="skillName">Name of the skill containing the function. The name can contain only alphanumeric chars + underscore.</param>
/// <param name="functionName">Name of the semantic function. The name can contain only alphanumeric chars + underscore.</param>
/// <param name="functionConfig">Function configuration, e.g. I/O params, AI settings, localization details, etc.</param>
/// <param name="trustService">Service used for trust checks (if null will use the default registered in the kernel).</param>
/// <returns>A C# function wrapping AI logic, usually defined with natural language</returns>
ISKFunction RegisterSemanticFunction(
string skillName,
string functionName,
SemanticFunctionConfig functionConfig,
ITrustService? trustService = null);
SemanticFunctionConfig functionConfig);
/// <summary>
/// Registers a custom function in the internal skill collection.
@@ -90,9 +78,8 @@ public interface IKernel
/// </summary>
/// <param name="skillInstance">Instance of a class containing functions</param>
/// <param name="skillName">Name of the skill for skill collection and prompt templates. If the value is empty functions are registered in the global namespace.</param>
/// <param name="trustService">Service used for trust checks (if null will use the default registered in the kernel).</param>
/// <returns>A list of all the semantic functions found in the directory, indexed by function name.</returns>
IDictionary<string, ISKFunction> ImportSkill(object skillInstance, string? skillName = null, ITrustService? trustService = null);
IDictionary<string, ISKFunction> ImportSkill(object skillInstance, string? skillName = null);
/// <summary>
/// Set the semantic memory to use
@@ -4,11 +4,9 @@ using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Security;
#pragma warning disable CA1710 // ContextVariables doesn't end in Dictionary or Collection
#pragma warning disable CA1725, RCS1168 // Uses "name" instead of "key" for some public APIs
@@ -23,24 +21,17 @@ namespace Microsoft.SemanticKernel.Orchestration;
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerTypeProxy(typeof(ContextVariables.TypeProxy))]
public sealed class ContextVariables : IDictionary<string, TrustAwareString>
public sealed class ContextVariables : IDictionary<string, string>
{
/// <summary>
/// Constructor for context variables.
/// </summary>
/// <param name="trustAwareContent">Optional value for the main variable of the context including trust information.</param>
public ContextVariables(TrustAwareString? trustAwareContent = null)
/// <param name="value">Optional value for the main variable of the context including trust information.</param>
public ContextVariables(string? value = null)
{
this._variables[MainKey] = trustAwareContent ?? TrustAwareString.Empty;
this._variables[MainKey] = value ?? string.Empty;
}
/// <summary>
/// Constructor for context variables.
/// By default the content will be trusted.
/// </summary>
/// <param name="content">Optional value for the main variable of the context.</param>
public ContextVariables(string? content) : this(TrustAwareString.CreateTrusted(content)) { }
/// <summary>
/// Create a copy of the current instance with a copy of the internal data
/// </summary>
@@ -48,7 +39,7 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
public ContextVariables Clone()
{
var clone = new ContextVariables();
foreach (KeyValuePair<string, TrustAwareString> x in this._variables)
foreach (KeyValuePair<string, string> x in this._variables)
{
clone.Set(x.Key, x.Value);
}
@@ -58,45 +49,21 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
/// <summary>Gets the main input string.</summary>
/// <remarks>If the main input string was removed from the collection, an empty string will be returned.</remarks>
public TrustAwareString Input => this._variables.TryGetValue(MainKey, out TrustAwareString? value) ? value : TrustAwareString.Empty;
public string Input => this._variables.TryGetValue(MainKey, out string? value) ? value : string.Empty;
/// <summary>
/// Updates the main input text with the new value after a function is complete.
/// The string includes trust information and will overwrite the trust state of the input.
/// </summary>
/// <param name="trustAwareContent">The new input value, for the next function in the pipeline, or as a result for the user
/// <param name="value">The new input value, for the next function in the pipeline, or as a result for the user
/// if the pipeline reached the end.</param>
/// <returns>The current instance</returns>
public ContextVariables Update(TrustAwareString? trustAwareContent)
public ContextVariables Update(string? value)
{
this._variables[MainKey] = trustAwareContent ?? TrustAwareString.Empty;
this._variables[MainKey] = value ?? string.Empty;
return this;
}
/// <summary>
/// Updates the main input text with the new value after a function is complete.
/// By default the content will be trusted.
/// </summary>
/// <param name="content">The new input value, for the next function in the pipeline, or as a result for the user
/// if the pipeline reached the end.</param>
/// <returns>The current instance</returns>
public ContextVariables Update(string? content)
{
return this.Update(TrustAwareString.CreateTrusted(content));
}
/// <summary>
/// Updates the main input text with the new value after a function is complete.
/// This will keep the trust state of the current input set.
/// </summary>
/// <param name="content">The new input value, for the next function in the pipeline, or as a result for the user
/// if the pipeline reached the end.</param>
/// <returns>The current instance</returns>
public ContextVariables UpdateKeepingTrustState(string? content)
{
return this.Update(new TrustAwareString(content, this.Input.IsTrusted));
}
/// <summary>
/// Updates all the local data with new data, merging the two datasets.
/// Do not discard old data
@@ -111,7 +78,7 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
// If requested, discard old data and keep only the new one.
if (!merge) { this._variables.Clear(); }
foreach (KeyValuePair<string, TrustAwareString> varData in newData._variables)
foreach (KeyValuePair<string, string> varData in newData._variables)
{
this._variables[varData.Key] = varData.Value;
}
@@ -127,13 +94,13 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
/// The string value includes trust information and will overwrite the trust information already stored for the variable.
/// </summary>
/// <param name="name">Variable name</param>
/// <param name="trustAwareValue">Value to store. If the value is NULL the variable is deleted.</param>
public void Set(string name, TrustAwareString? trustAwareValue)
/// <param name="value">Value to store. If the value is NULL the variable is deleted.</param>
public void Set(string name, string? value)
{
Verify.NotNullOrWhiteSpace(name);
if (trustAwareValue != null)
if (value != null)
{
this._variables[name] = trustAwareValue;
this._variables[name] = value;
}
else
{
@@ -141,68 +108,6 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
}
}
/// <summary>
/// This method allows to store additional data in the context variables, e.g. variables needed by functions in the
/// pipeline. These "variables" are visible also to semantic functions using the "{{varName}}" syntax, allowing
/// to inject more information into prompt templates.
/// By default the variables' value will be trusted.
/// </summary>
/// <param name="name">Variable name</param>
/// <param name="value">Value to store</param>
public void Set(string name, string value)
{
this.Set(name, TrustAwareString.CreateTrusted(value));
}
/// <summary>
/// Fetch a variable value and if its content is trusted from the context variables.
/// </summary>
/// <param name="name">Variable name</param>
/// <param name="trustAwareValue">Variable value as a string with trust information</param>
/// <returns>Whether the value exists in the context variables</returns>
[Obsolete("This method is deprecated and will be removed in one of the next SK SDK versions. Use the TryGetValue method or indexer instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool Get(string name, out TrustAwareString trustAwareValue)
{
if (this._variables.TryGetValue(name, out TrustAwareString result))
{
trustAwareValue = result;
return true;
}
trustAwareValue = TrustAwareString.Empty;
return false;
}
/// <summary>
/// Fetch a variable value from the context variables.
/// </summary>
/// <param name="name">Variable name</param>
/// <param name="value">Value</param>
/// <returns>Whether the value exists in the context variables</returns>
[Obsolete("This method is deprecated and will be removed in one of the next SK SDK versions. Use the TryGetValue method or indexer instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool Get(string name, out string value)
{
var exists = this.Get(name, out TrustAwareString trustAwareValue);
value = trustAwareValue.Value;
return exists;
}
/// <summary>
/// Gets the variable value associated with the specified name.
/// </summary>
/// <param name="name">The name of the variable to get.</param>
/// <param name="value">
/// When this method returns, contains the variable value associated with the specified name, if the variable is found;
/// otherwise, null.
/// </param>
/// <returns>true if the <see cref="ContextVariables"/> contains a variable with the specified name; otherwise, false.</returns>
public bool TryGetValue(string name, [NotNullWhen(true)] out TrustAwareString? value) =>
this._variables.TryGetValue(name, out value);
/// <summary>
/// Gets the variable value associated with the specified name.
/// </summary>
@@ -214,9 +119,8 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
/// <returns>true if the <see cref="ContextVariables"/> contains a variable with the specified name; otherwise, false.</returns>
public bool TryGetValue(string name, [NotNullWhen(true)] out string? value)
{
if (this._variables.TryGetValue(name, out TrustAwareString? trustAwareValue))
if (this._variables.TryGetValue(name, out value))
{
value = trustAwareValue.Value;
return true;
}
@@ -231,14 +135,10 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
/// <returns>The value of the variable.</returns>
public string this[string name]
{
get => this._variables[name].Value;
get => this._variables[name];
set
{
// This will be trusted by default for now.
// TODO: we could plan to replace string usages in the kernel
// with TrustAwareString, so here "value" could directly be a trust aware string
// including trust information
this._variables[name] = TrustAwareString.CreateTrusted(value);
this._variables[name] = value;
}
}
@@ -252,42 +152,6 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
return this._variables.ContainsKey(name);
}
/// <summary>
/// True if all the stored variables have trusted content.
/// </summary>
/// <returns></returns>
public bool IsAllTrusted()
{
foreach (var pair in this._variables)
{
if (!pair.Value.IsTrusted)
{
return false;
}
}
return true;
}
/// <summary>
/// Make all the variables stored in the context untrusted.
/// </summary>
public void UntrustAll()
{
foreach (var item in this._variables)
{
// Note: we don't use an internal setter for better multi-threading
this._variables[item.Key] = TrustAwareString.CreateUntrusted(item.Value.Value);
}
}
/// <summary>
/// Make the input variable untrusted.
/// </summary>
public void UntrustInput()
{
this.Update(TrustAwareString.CreateUntrusted(this.Input.Value));
}
/// <summary>
/// Print the processed input, aka the current data after any processing occurred.
/// </summary>
@@ -298,24 +162,24 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
/// Get an enumerator that iterates through the context variables.
/// </summary>
/// <returns>An enumerator that iterates through the context variables</returns>
public IEnumerator<KeyValuePair<string, TrustAwareString>> GetEnumerator() => this._variables.GetEnumerator();
public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => this._variables.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => this._variables.GetEnumerator();
void IDictionary<string, TrustAwareString>.Add(string key, TrustAwareString value) => ((IDictionary<string, TrustAwareString>)this._variables).Add(key, value);
bool IDictionary<string, TrustAwareString>.Remove(string key) => ((IDictionary<string, TrustAwareString>)this._variables).Remove(key);
void ICollection<KeyValuePair<string, TrustAwareString>>.Add(KeyValuePair<string, TrustAwareString> item) => ((ICollection<KeyValuePair<string, TrustAwareString>>)this._variables).Add(item);
void ICollection<KeyValuePair<string, TrustAwareString>>.Clear() => ((ICollection<KeyValuePair<string, TrustAwareString>>)this._variables).Clear();
bool ICollection<KeyValuePair<string, TrustAwareString>>.Contains(KeyValuePair<string, TrustAwareString> item) => ((ICollection<KeyValuePair<string, TrustAwareString>>)this._variables).Contains(item);
void ICollection<KeyValuePair<string, TrustAwareString>>.CopyTo(KeyValuePair<string, TrustAwareString>[] array, int arrayIndex) => ((ICollection<KeyValuePair<string, TrustAwareString>>)this._variables).CopyTo(array, arrayIndex);
bool ICollection<KeyValuePair<string, TrustAwareString>>.Remove(KeyValuePair<string, TrustAwareString> item) => ((ICollection<KeyValuePair<string, TrustAwareString>>)this._variables).Remove(item);
ICollection<string> IDictionary<string, TrustAwareString>.Keys => ((IDictionary<string, TrustAwareString>)this._variables).Keys;
ICollection<TrustAwareString> IDictionary<string, TrustAwareString>.Values => ((IDictionary<string, TrustAwareString>)this._variables).Values;
int ICollection<KeyValuePair<string, TrustAwareString>>.Count => ((ICollection<KeyValuePair<string, TrustAwareString>>)this._variables).Count;
bool ICollection<KeyValuePair<string, TrustAwareString>>.IsReadOnly => ((ICollection<KeyValuePair<string, TrustAwareString>>)this._variables).IsReadOnly;
TrustAwareString IDictionary<string, TrustAwareString>.this[string key]
void IDictionary<string, string>.Add(string key, string value) => ((IDictionary<string, string>)this._variables).Add(key, value);
bool IDictionary<string, string>.Remove(string key) => ((IDictionary<string, string>)this._variables).Remove(key);
void ICollection<KeyValuePair<string, string>>.Add(KeyValuePair<string, string> item) => ((ICollection<KeyValuePair<string, string>>)this._variables).Add(item);
void ICollection<KeyValuePair<string, string>>.Clear() => ((ICollection<KeyValuePair<string, string>>)this._variables).Clear();
bool ICollection<KeyValuePair<string, string>>.Contains(KeyValuePair<string, string> item) => ((ICollection<KeyValuePair<string, string>>)this._variables).Contains(item);
void ICollection<KeyValuePair<string, string>>.CopyTo(KeyValuePair<string, string>[] array, int arrayIndex) => ((ICollection<KeyValuePair<string, string>>)this._variables).CopyTo(array, arrayIndex);
bool ICollection<KeyValuePair<string, string>>.Remove(KeyValuePair<string, string> item) => ((ICollection<KeyValuePair<string, string>>)this._variables).Remove(item);
ICollection<string> IDictionary<string, string>.Keys => ((IDictionary<string, string>)this._variables).Keys;
ICollection<string> IDictionary<string, string>.Values => ((IDictionary<string, string>)this._variables).Values;
int ICollection<KeyValuePair<string, string>>.Count => ((ICollection<KeyValuePair<string, string>>)this._variables).Count;
bool ICollection<KeyValuePair<string, string>>.IsReadOnly => ((ICollection<KeyValuePair<string, string>>)this._variables).IsReadOnly;
string IDictionary<string, string>.this[string key]
{
get => ((IDictionary<string, TrustAwareString>)this._variables)[key];
set => ((IDictionary<string, TrustAwareString>)this._variables)[key] = value;
get => ((IDictionary<string, string>)this._variables)[key];
set => ((IDictionary<string, string>)this._variables)[key] = value;
}
internal const string MainKey = "INPUT";
@@ -331,7 +195,7 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
/// <summary>
/// Important: names are case insensitive
/// </summary>
private readonly ConcurrentDictionary<string, TrustAwareString> _variables = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, string> _variables = new(StringComparer.OrdinalIgnoreCase);
private sealed class TypeProxy
{
@@ -340,7 +204,7 @@ public sealed class ContextVariables : IDictionary<string, TrustAwareString>
public TypeProxy(ContextVariables variables) => this._variables = variables;
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<string, TrustAwareString>[] Items => this._variables._variables.ToArray();
public KeyValuePair<string, string>[] Items => this._variables._variables.ToArray();
}
#endregion
@@ -29,11 +29,6 @@ public sealed class SKContext
/// <returns>Processed input, aka result</returns>
public string Result => this.Variables.ToString();
/// <summary>
/// Whether all the context variables are trusted or not.
/// </summary>
public bool IsTrusted => this.Variables.IsAllTrusted();
/// <summary>
/// Whether an error occurred while executing functions in the pipeline.
/// </summary>
@@ -156,22 +151,6 @@ public sealed class SKContext
this._culture = CultureInfo.CurrentCulture;
}
/// <summary>
/// Make all the variables stored in the context untrusted.
/// </summary>
public void UntrustAll()
{
this.Variables.UntrustAll();
}
/// <summary>
/// Make the result untrusted.
/// </summary>
public void UntrustResult()
{
this.Variables.UntrustInput();
}
/// <summary>
/// Print the processed input, aka the current data after any processing occurred.
/// If an error occurred, prints the last exception message instead.
@@ -1,69 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
namespace Microsoft.SemanticKernel.Security;
/// <summary>
/// Base interface used to handle trust events and validation. The flow in the SKFunction is:
///
/// - Semantic function
/// - Call ValidateContextAsync
/// - Render prompt using template and variables
/// - Call ValidatePromptAsync with rendered template and context
/// - Call completion client with the returned prompt from ValidatePromptAsync
/// - Update result
///
/// - Native function
/// - Call ValidateContextAsync
/// - Call native function implementation
/// - Update result
/// </summary>
public interface ITrustService
{
/// <summary>
/// Called by the SKFunction flow to validate if the current context is considered to be trusted or not:
///
/// - This is called for semantic functions before rendering the prompt template.
/// - This is called for native functions before calling the native function implementation.
///
/// If the return is false, this means the result of the function will be tagged as untrusted.
///
/// The implementation might depend on the application needs. A simple sample implementation
/// could be accomplished by analyzing the variables in the context, if they are all trusted, then
/// consider the context to be trusted.
///
/// This also gives an opportunity for the context to be updated or actions to be taken if
/// potentially untrusted content is found. For example, sanitizing an untrusted variable and turning it into trusted.
/// </summary>
/// <param name="func">Instance of the function being called</param>
/// <param name="context">The current execution context</param>
/// <returns>Should return whether the result of the function should be considered trusted depending on the context</returns>
Task<bool> ValidateContextAsync(ISKFunction func, SKContext context);
/// <summary>
/// This will only be called by semantic functions. It will be called in the SKFunction flow after the prompt is
/// rendered using the given template, and before calling the text completion with the rendered prompt.
///
/// It should return the content to be used in the completion client as a TrustAwareString, which will include
/// trust information. If the TrustAwareString returned is not trusted, this means the result of the function will be tagged as untrusted.
///
/// After the template is rendered, the context might be tagged as untrusted because the template might contain function calls
/// that turned the context into untrusted when rendered.
///
/// The implementation might depend on the application needs. A simple sample implementation
/// could be accomplished by analyzing the variables in the context and the rendered prompt, if everything is trusted, then
/// return the prompt wrapped in a TrustAwareString and tagged as trusted.
///
/// This also gives an opportunity for both the context and the prompt to be updated before calling the completion client
/// when something untrusted is identified. For example, sanitizing an untrusted prompt and turning it into trusted.
/// </summary>
/// <param name="func">Instance of the function being called</param>
/// <param name="context">The current execution context</param>
/// <param name="prompt">The current rendered prompt to be used with the completion client</param>
/// <returns>Should return a TrustAwareString representing the final prompt to be used for the completion client.
/// The TrustAwareString includes trust information</returns>
Task<TrustAwareString> ValidatePromptAsync(ISKFunction func, SKContext context, string prompt);
}
@@ -1,106 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics;
namespace Microsoft.SemanticKernel.Security;
/// <summary>
/// Provides an immutable string that carries trust information.
/// </summary>
[DebuggerDisplay("Value = {Value}, IsTrusted = {IsTrusted}")]
public sealed class TrustAwareString : IEquatable<TrustAwareString>
{
/// <summary>
/// Gets a trusted empty string.
/// </summary>
public static TrustAwareString Empty { get; } = new(string.Empty, isTrusted: true);
/// <summary>
/// Create a new trusted string.
/// </summary>
/// <param name="value">The raw string value</param>
/// <returns>TrustAwareString</returns>
public static TrustAwareString CreateTrusted(string? value) => new(value, isTrusted: true);
/// <summary>
/// Create a new untrusted string.
/// </summary>
/// <param name="value">The raw string value</param>
/// <returns>TrustAwareString</returns>
public static TrustAwareString CreateUntrusted(string? value) => new(value, isTrusted: false);
/// <summary>
/// The raw string value.
/// </summary>
public string Value { get; }
/// <summary>
/// Whether the current value is trusted or not.
/// </summary>
public bool IsTrusted { get; }
/// <summary>
/// Create a new trust aware string.
/// </summary>
/// <param name="value">The raw string value</param>
/// <param name="isTrusted">Whether the raw string value is trusted or not</param>
public TrustAwareString(string? value, bool isTrusted = true)
{
this.Value = value ?? string.Empty;
this.IsTrusted = isTrusted;
}
public override string ToString()
{
return this.Value;
}
public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}
else if (obj is string str)
{
return this.Value == str;
}
else
{
return (obj is TrustAwareString trustAwareStr) && this.Equals(trustAwareStr);
}
}
public bool Equals(TrustAwareString? other)
{
if (other is null)
{
return false;
}
return this.Value == other.Value && this.IsTrusted == other.IsTrusted;
}
public override int GetHashCode()
{
return HashCode.Combine(this.Value, this.IsTrusted);
}
public static bool operator ==(TrustAwareString? left, TrustAwareString? right)
{
if (left is null)
{
return right is null;
}
return left.Equals(right);
}
public static bool operator !=(TrustAwareString? left, TrustAwareString? right)
{
return !(left == right);
}
public static implicit operator string(TrustAwareString s) => s.Value;
}
@@ -1,76 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
namespace Microsoft.SemanticKernel.Security;
/// <summary>
/// Default implementation of the trust service.
///
/// This is just a simple example implementation that will be used by default if no other is provided.
///
/// When set, throws an exception to stop execution when sensitive functions try to run with untrusted content.
/// </summary>
public sealed class TrustService : ITrustService
{
/// <summary>
/// Creates the default trusted implementation of the trust service.
/// The default trusted version will use the trust information of the variables in the context to decide
/// whether the result of the function call should be trusted or not.
/// </summary>
public static TrustService DefaultTrusted => new(true);
/// <summary>
/// Creates the default untrusted implementation of the trust service.
/// The default untrusted version will always force the result of the function call to be untrusted.
/// </summary>
public static TrustService DefaultUntrusted => new(false);
/// <summary>
/// If set to:
/// - false: will cause the context/prompt to always be considered untrusted, meaning the output of the function will always be considered untrusted.
/// - true: will use the trust information of the variables in the context to decide whether the context/prompt is trusted or not
/// (trusted only if all the variables within the context are trusted).
/// </summary>
private readonly bool _defaultTrusted;
/// <inheritdoc/>
public Task<bool> ValidateContextAsync(ISKFunction func, SKContext context)
{
return Task.FromResult(this.InternalValidation(func, context));
}
/// <inheritdoc/>
public Task<TrustAwareString> ValidatePromptAsync(ISKFunction func, SKContext context, string prompt)
{
var isTrusted = this.InternalValidation(func, context);
return Task.FromResult(new TrustAwareString(
// This is only a sample implementation that directly returns the prompt
prompt,
// The content of the prompt will not be used in this example validation
isTrusted: isTrusted
));
}
private TrustService(bool defaultTrusted)
{
this._defaultTrusted = defaultTrusted;
}
private bool InternalValidation(ISKFunction func, SKContext context)
{
bool contextIsTrusted = context.IsTrusted;
if (func.IsSensitive && !contextIsTrusted)
{
throw new UntrustedContentException(
UntrustedContentException.ErrorCodes.SensitiveFunctionWithUntrustedContent,
$"Could not run {func.SkillName}.{func.Name}, the function is sensitive and the input untrusted"
);
}
// If defaultTrusted == false, will always return false
return contextIsTrusted && this._defaultTrusted;
}
}
@@ -1,80 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.SemanticKernel.Diagnostics;
namespace Microsoft.SemanticKernel.Security;
#pragma warning disable RCS1194 // Implement standard exception constructors
/// <summary>
/// Untrusted content exception, used to warn about:
/// - untrusted content in the context passed to any function
/// - untrusted prompts when using semantic functions
/// </summary>
public class UntrustedContentException : SKException
{
/// <summary>
/// Initializes a new instance of the <see cref="UntrustedContentException"/> class with a provided error code.
/// </summary>
/// <param name="errorCode">The error code.</param>
public UntrustedContentException(ErrorCodes errorCode)
: this(errorCode, message: null, innerException: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UntrustedContentException"/> class with a provided error code and message.
/// </summary>
/// <param name="errorCode">The error code.</param>
/// <param name="message">The exception message.</param>
public UntrustedContentException(ErrorCodes errorCode, string? message)
: this(errorCode, message, innerException: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UntrustedContentException"/> class with a provided error code, message, and inner exception.
/// </summary>
/// <param name="errorCode">The error code.</param>
/// <param name="message">A string that describes the error.</param>
/// <param name="innerException">The exception that is the cause of the current exception.</param>
public UntrustedContentException(ErrorCodes errorCode, string? message, Exception? innerException)
: base(GetDefaultMessage(errorCode, message), innerException)
{
this.ErrorCode = errorCode;
}
/// <summary>
/// Gets the error code for this exception.
/// </summary>
public ErrorCodes ErrorCode { get; }
/// <summary>Translate the error code into a default message.</summary>
private static string GetDefaultMessage(ErrorCodes errorCode, string? message)
{
string description = errorCode switch
{
ErrorCodes.SensitiveFunctionWithUntrustedContent => "Sensitive function with untrusted content",
_ => $"Unknown error ({errorCode:G})",
};
return message is not null ? $"{description}: {message}" : description;
}
/// <summary>
/// Error codes for <see cref="UntrustedContentException"/>.
/// </summary>
public enum ErrorCodes
{
/// <summary>
/// Unknown error.
/// </summary>
UnknownError = -1,
/// <summary>
/// Sensitive function was called with untrusted content.
/// </summary>
SensitiveFunctionWithUntrustedContent,
}
}
@@ -56,7 +56,7 @@ public class PromptTemplateConfig
/// </summary>
[JsonPropertyName("max_tokens")]
[JsonPropertyOrder(5)]
public int MaxTokens { get; set; } = 256;
public int? MaxTokens { get; set; }
/// <summary>
/// Stop sequences are optional sequences that tells the AI model when to stop generating tokens.
@@ -152,15 +152,6 @@ public class PromptTemplateConfig
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public InputConfig Input { get; set; } = new();
/// <summary>
/// Whether the function is sensitive (default false).
/// When a function is sensitive, the default trust service will throw an exception
/// if the function is invoked passing in some untrusted input (or context, or prompt).
/// </summary>
[JsonPropertyName("is_sensitive")]
[JsonPropertyOrder(7)]
public bool IsSensitive { get; set; } = false;
/// <summary>
/// Remove some default properties to reduce the JSON complexity.
/// </summary>
@@ -7,7 +7,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
namespace Microsoft.SemanticKernel.SkillDefinition;
@@ -38,19 +37,6 @@ public interface ISKFunction
/// </summary>
bool IsSemantic { get; }
/// <summary>
/// Whether the function is set to be sensitive (default false).
/// When a function is sensitive, the default trust service will throw an exception
/// if the function is invoked passing in some untrusted input (or context, or prompt).
/// </summary>
bool IsSensitive { get; }
/// <summary>
/// Service used for trust check events.
/// This can be provided at function creation, if not, the TrustService.DefaultTrusted implementation will be used.
/// </summary>
ITrustService TrustServiceInstance { get; }
/// <summary>
/// AI service settings
/// </summary>
@@ -52,27 +52,19 @@ namespace Microsoft.SemanticKernel.SkillDefinition;
public sealed class SKFunctionAttribute : Attribute
{
/// <summary>Initializes the attribute.</summary>
/// <param name="isSensitive">Whether the function is set to be sensitive (default false).</param>
public SKFunctionAttribute(bool isSensitive = false) => this.IsSensitive = isSensitive;
/// <summary>
/// Whether the function is set to be sensitive (default false).
/// When a function is sensitive, the default trust service will throw an exception
/// if the function is invoked passing in some untrusted input (or context, or prompt).
/// </summary>
public bool IsSensitive { get; }
public SKFunctionAttribute()
{
}
/// <summary>
/// Initializes the attribute with the specified description.
/// </summary>
/// <param name="description">Description of the function to be used by a planner to auto-discover functions.</param>
/// <param name="isSensitive">Whether the function is set to be sensitive (default false).</param>
[Obsolete("This constructor is deprecated and will be removed in one of the next SK SDK versions.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public SKFunctionAttribute(string description, bool isSensitive = false)
public SKFunctionAttribute(string description)
{
this.Description = description;
this.IsSensitive = isSensitive;
}
[Obsolete("This property is deprecated and will be removed in one of the next SK SDK versions.")]
@@ -9,8 +9,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.SkillDefinition;
using Moq;
using Xunit;
@@ -50,31 +48,6 @@ public class KernelTests
Assert.Equal(3, data.NativeFunctions["mySk"].Count);
}
[Fact]
[System.Obsolete("This method is deprecated and will be removed in one of the next SK SDK versions.")]
public async Task ItProvidesAccessToFunctionsViaSKContextObsoleteAsync()
{
// Arrange
var kernel = KernelBuilder.Create();
var factory = new Mock<Func<IKernel, ITextCompletion>>();
kernel.Config.AddTextCompletionService(factory.Object);
var nativeSkill = new MySkill();
kernel.CreateSemanticFunction("Tell me a joke", functionName: "joker", skillName: "jk", description: "Nice fun");
var skill = kernel.ImportSkill(nativeSkill, "mySk");
// Act
SKContext result = await kernel.RunAsync(skill["ReadSkillCollectionAsync"]);
// Assert - 3 functions, var name is not case sensitive
Assert.Equal("Nice fun", result["jk.joker"]);
Assert.Equal("Nice fun", result["JK.JOKER"]);
Assert.Equal("Just say hello", result["mySk.sayhello"]);
Assert.Equal("Just say hello", result["mySk.SayHello"]);
Assert.Equal("Export info.", result["mySk.ReadSkillCollectionAsync"]);
Assert.Equal("Export info.", result["mysk.readskillcollectionasync"]);
}
[Fact]
public void ItProvidesAccessToFunctionsViaSkillCollection()
{
@@ -208,109 +181,6 @@ public class KernelTests
kernel.ImportSkill(new MySkill());
}
[Fact]
public void ItConfiguresCustomTrustServiceInFunctionsBySettingKernelTrustService()
{
// Arrange
ITrustService trustService = new CustomTrustService();
var factory = new Mock<Func<(ILogger, KernelConfig), ITextCompletion>>();
var kernel = Kernel.Builder
.WithAIService<ITextCompletion>("x", factory.Object)
.WithTrustService(trustService).Build();
var promptTemplateConfig = new PromptTemplateConfig() { IsSensitive = true };
var promptTemplate = new PromptTemplate("Tell me a joke", promptTemplateConfig, kernel);
var semanticFunctionConfig = new SemanticFunctionConfig(promptTemplateConfig, promptTemplate);
// Act
var nativeSkill = kernel.ImportSkill(new MySkill(), "mySk");
var semanticSkill0 = kernel.RegisterSemanticFunction(functionName: "joker0", semanticFunctionConfig);
var semanticSkill1 = kernel.RegisterSemanticFunction(skillName: "jk", functionName: "joker1", semanticFunctionConfig);
var semanticSkill2 = kernel.CreateSemanticFunction("Tell me a joke", functionName: "joker2", skillName: "jk", description: "Nice fun");
var sayHelloFunc = (SKFunction)kernel.Skills.GetFunction("mySk", "SayHello");
var jokerFunc0 = (SKFunction)kernel.Skills.GetFunction("joker0");
var jokerFunc1 = (SKFunction)kernel.Skills.GetFunction("jk", "joker1");
var jokerFunc2 = (SKFunction)kernel.Skills.GetFunction("jk", "joker2");
// Assert
Assert.NotNull(kernel.TrustServiceInstance);
Assert.NotNull(sayHelloFunc.TrustServiceInstance);
Assert.NotNull(jokerFunc0.TrustServiceInstance);
Assert.NotNull(jokerFunc1.TrustServiceInstance);
Assert.NotNull(jokerFunc1.TrustServiceInstance);
Assert.Equal(trustService, kernel.TrustServiceInstance);
Assert.Equal(trustService, sayHelloFunc.TrustServiceInstance);
Assert.Equal(trustService, jokerFunc0.TrustServiceInstance);
Assert.Equal(trustService, jokerFunc1.TrustServiceInstance);
Assert.Equal(trustService, jokerFunc2.TrustServiceInstance);
}
[Fact]
public void ItConfiguresCustomTrustServiceInFunctionsByFunctionCreationParameter()
{
// Arrange
ITrustService trustService = new CustomTrustService();
var factory = new Mock<Func<(ILogger, KernelConfig), ITextCompletion>>();
var kernel = Kernel.Builder
.WithAIService<ITextCompletion>("x", factory.Object).Build();
var promptTemplateConfig = new PromptTemplateConfig { IsSensitive = true };
var promptTemplate = new PromptTemplate("Tell me a joke", promptTemplateConfig, kernel);
var semanticFunctionConfig = new SemanticFunctionConfig(promptTemplateConfig, promptTemplate);
// Act
var nativeSkill = kernel.ImportSkill(new MySkill(), "mySk", trustService: trustService);
var semanticSkill0 = kernel.RegisterSemanticFunction(functionName: "joker0", semanticFunctionConfig, trustService: trustService);
var semanticSkill1 = kernel.RegisterSemanticFunction(skillName: "jk", functionName: "joker1", semanticFunctionConfig, trustService: trustService);
var semanticSkill2 = kernel.CreateSemanticFunction("Tell me a joke", functionName: "joker2", skillName: "jk", description: "Nice fun", trustService: trustService);
var sayHelloFunc = (SKFunction)kernel.Skills.GetFunction("mySk", "SayHello");
var jokerFunc0 = (SKFunction)kernel.Skills.GetFunction("joker0");
var jokerFunc1 = (SKFunction)kernel.Skills.GetFunction("jk", "joker1");
var jokerFunc2 = (SKFunction)kernel.Skills.GetFunction("jk", "joker2");
// Assert
Assert.Null(kernel.TrustServiceInstance);
Assert.NotNull(sayHelloFunc.TrustServiceInstance);
Assert.NotNull(jokerFunc0.TrustServiceInstance);
Assert.NotNull(jokerFunc1.TrustServiceInstance);
Assert.NotNull(jokerFunc1.TrustServiceInstance);
Assert.Equal(trustService, sayHelloFunc.TrustServiceInstance);
Assert.Equal(trustService, jokerFunc0.TrustServiceInstance);
Assert.Equal(trustService, jokerFunc1.TrustServiceInstance);
Assert.Equal(trustService, jokerFunc2.TrustServiceInstance);
}
[Fact]
public void ItUsesDefaultTrustServiceInFunctionsIfNoneIsProvided()
{
// Arrange
var factory = new Mock<Func<(ILogger, KernelConfig), ITextCompletion>>();
var kernel = Kernel.Builder
.WithAIService<ITextCompletion>("x", factory.Object).Build();
var promptTemplateConfig = new PromptTemplateConfig { IsSensitive = true };
var promptTemplate = new PromptTemplate("Tell me a joke", promptTemplateConfig, kernel);
var semanticFunctionConfig = new SemanticFunctionConfig(promptTemplateConfig, promptTemplate);
// Act
var nativeSkill = kernel.ImportSkill(new MySkill(), "mySk");
var semanticSkill0 = kernel.RegisterSemanticFunction(functionName: "joker0", semanticFunctionConfig);
var semanticSkill1 = kernel.RegisterSemanticFunction(skillName: "jk", functionName: "joker1", semanticFunctionConfig);
var semanticSkill2 = kernel.CreateSemanticFunction("Tell me a joke", functionName: "joker2", skillName: "jk", description: "Nice fun");
var sayHelloFunc = (SKFunction)kernel.Skills.GetFunction("mySk", "SayHello");
var jokerFunc0 = (SKFunction)kernel.Skills.GetFunction("joker0");
var jokerFunc1 = (SKFunction)kernel.Skills.GetFunction("jk", "joker1");
var jokerFunc2 = (SKFunction)kernel.Skills.GetFunction("jk", "joker2");
// Assert
Assert.Null(kernel.TrustServiceInstance);
Assert.NotNull(sayHelloFunc.TrustServiceInstance);
Assert.NotNull(jokerFunc0.TrustServiceInstance);
Assert.NotNull(jokerFunc1.TrustServiceInstance);
Assert.NotNull(jokerFunc1.TrustServiceInstance);
Assert.IsType<TrustService>(sayHelloFunc.TrustServiceInstance);
Assert.IsType<TrustService>(jokerFunc0.TrustServiceInstance);
Assert.IsType<TrustService>(jokerFunc1.TrustServiceInstance);
Assert.IsType<TrustService>(jokerFunc2.TrustServiceInstance);
}
public class MySkill
{
[SKFunction, Description("Return any value.")]
@@ -356,17 +226,4 @@ public class KernelTests
return context;
}
}
private sealed class CustomTrustService : ITrustService
{
public Task<bool> ValidateContextAsync(ISKFunction func, SKContext context)
{
throw new NotImplementedException();
}
public Task<TrustAwareString> ValidatePromptAsync(ISKFunction func, SKContext context, string prompt)
{
throw new NotImplementedException();
}
}
}
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Xunit;
namespace SemanticKernel.UnitTests.Orchestration;
@@ -106,7 +105,6 @@ public class ContextVariablesTests
// Assert
Assert.True(target.TryGetValue(anyName, out string? _));
Assert.True(target.TryGetValue(anyName, out TrustAwareString? _));
}
[Fact]
@@ -123,7 +121,6 @@ public class ContextVariablesTests
// Assert
Assert.True(target.TryGetValue(anyName, out string? _));
Assert.True(target.TryGetValue(anyName, out TrustAwareString? _));
}
[Fact]
@@ -140,7 +137,6 @@ public class ContextVariablesTests
// Assert
Assert.True(target.TryGetValue(anyName, out string? _));
Assert.True(target.TryGetValue(anyName, out TrustAwareString? _));
}
[Fact]
@@ -157,7 +153,6 @@ public class ContextVariablesTests
// Assert
Assert.True(target.TryGetValue(anyName, out string? _));
Assert.True(target.TryGetValue(anyName, out TrustAwareString? _));
}
[Fact]
@@ -173,7 +168,6 @@ public class ContextVariablesTests
// Assert
Assert.True(target.TryGetValue(anyName, out string? _));
Assert.True(target.TryGetValue(anyName, out TrustAwareString? _));
}
[Fact]
@@ -189,7 +183,6 @@ public class ContextVariablesTests
// Assert
Assert.True(target.TryGetValue(anyName, out string? _));
Assert.True(target.TryGetValue(anyName, out TrustAwareString? _));
}
[Fact]
@@ -204,34 +197,13 @@ public class ContextVariablesTests
target.Set(anyName, anyContent);
// Assert
AssertContextVariable(target, anyName, anyContent, true);
AssertContextVariable(target, anyName, anyContent);
// Act - Erase entry
target.Set(anyName, null);
// Assert - Should have been erased
Assert.False(target.TryGetValue(anyName, out string? _));
Assert.False(target.TryGetValue(anyName, out TrustAwareString? _));
}
[Fact]
public void GetWithTrustAwareStringSucceeds()
{
// Arrange
string mainContent = Guid.NewGuid().ToString();
string anyName = Guid.NewGuid().ToString();
string anyContent = Guid.NewGuid().ToString();
ContextVariables target = new(mainContent);
// Act
target.Set(anyName, TrustAwareString.CreateUntrusted(anyContent));
// Assert
Assert.True(target.TryGetValue(anyName, out TrustAwareString? trustAwareValue));
Assert.NotNull(trustAwareValue);
Assert.Equal(anyContent, trustAwareValue.Value);
Assert.False(trustAwareValue.IsTrusted);
Assert.Equal(mainContent, target.Input.Value);
}
[Fact]
@@ -244,167 +216,12 @@ public class ContextVariablesTests
ContextVariables target = new(mainContent);
// Act
target.Set(anyName, TrustAwareString.CreateUntrusted(anyContent));
target.Set(anyName, anyContent);
// Assert
Assert.True(target.TryGetValue(anyName, out string? value));
Assert.Equal(anyContent, value);
Assert.Equal(mainContent, target.Input.Value);
}
[Fact]
public void CreateWithInputDefaultIsTrustedSucceeds()
{
// Arrange
string anyContent = Guid.NewGuid().ToString();
ContextVariables target = new(anyContent);
// Assert
Assert.Equal(anyContent, target.Input);
AssertContextVariable(target, ContextVariables.MainKey, anyContent, true);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CreateWithInputIsTrustedValueSucceeds(bool isTrusted)
{
// Arrange
string anyContent = Guid.NewGuid().ToString();
ContextVariables target = new(new TrustAwareString(anyContent, isTrusted));
// Assert
Assert.Equal(anyContent, target.Input);
AssertContextVariable(target, ContextVariables.MainKey, anyContent, isTrusted);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void KeepInputContentAndUpdateIsTrustedSucceeds(bool isTrusted)
{
// Arrange
string anyContent = Guid.NewGuid().ToString();
ContextVariables target = new(new TrustAwareString(anyContent, !isTrusted));
// Assert
Assert.Equal(anyContent, target.Input);
AssertContextVariable(target, ContextVariables.MainKey, anyContent, !isTrusted);
// Act
target.Update(new TrustAwareString(target.Input, isTrusted));
// Assert
Assert.Equal(anyContent, target.Input);
AssertContextVariable(target, ContextVariables.MainKey, anyContent, isTrusted);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void UpdateInputContentAndUpdateIsTrustedSucceeds(bool isTrusted)
{
// Arrange
string anyContent = Guid.NewGuid().ToString();
string newContent = Guid.NewGuid().ToString();
ContextVariables target = new(new TrustAwareString(anyContent, !isTrusted));
// Assert
Assert.Equal(anyContent, target.Input);
AssertContextVariable(target, ContextVariables.MainKey, anyContent, !isTrusted);
// Act
target.Update(new TrustAwareString(newContent, isTrusted));
// Assert
Assert.Equal(newContent, target.Input);
AssertContextVariable(target, ContextVariables.MainKey, newContent, isTrusted);
}
[Fact]
public void UpdateWithStringDefaultsToTrustedSucceeds()
{
// Arrange
string content0 = Guid.NewGuid().ToString();
string content1 = Guid.NewGuid().ToString();
ContextVariables trustedVar = new(TrustAwareString.CreateTrusted(Guid.NewGuid().ToString()));
ContextVariables untrustedVar = new(TrustAwareString.CreateUntrusted(Guid.NewGuid().ToString()));
// Act
trustedVar.Update(content0);
untrustedVar.Update(content1);
// Assert
AssertContextVariable(trustedVar, ContextVariables.MainKey, content0, true);
AssertContextVariable(untrustedVar, ContextVariables.MainKey, content1, true);
}
[Fact]
public void UpdateWithNullDefaultsToEmptyTrustedSucceeds()
{
// Arrange
ContextVariables untrustedVar = new(TrustAwareString.CreateUntrusted(Guid.NewGuid().ToString()));
// Act
untrustedVar.Update(null);
// Assert
AssertContextVariable(untrustedVar, ContextVariables.MainKey, string.Empty, true);
}
[Fact]
public void UpdateUntrustedSucceeds()
{
// Arrange
string trustedContent = Guid.NewGuid().ToString();
string untrustedContent = Guid.NewGuid().ToString();
ContextVariables trustedVar = new(TrustAwareString.CreateTrusted(Guid.NewGuid().ToString()));
ContextVariables untrustedVar = new(TrustAwareString.CreateUntrusted(Guid.NewGuid().ToString()));
// Act
trustedVar.Update(TrustAwareString.CreateUntrusted(trustedContent));
untrustedVar.Update(TrustAwareString.CreateUntrusted(untrustedContent));
// Assert
AssertContextVariable(trustedVar, ContextVariables.MainKey, trustedContent, false);
AssertContextVariable(untrustedVar, ContextVariables.MainKey, untrustedContent, false);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void UpdateKeepingTrustStateSucceeds(bool isTrusted)
{
// Arrange
string someContent = Guid.NewGuid().ToString();
string someNewContent = Guid.NewGuid().ToString();
ContextVariables variables = new(new TrustAwareString(someContent, isTrusted));
// Assert
AssertContextVariable(variables, ContextVariables.MainKey, someContent, isTrusted);
// Act
variables.UpdateKeepingTrustState(someNewContent);
// Assert
AssertContextVariable(variables, ContextVariables.MainKey, someNewContent, isTrusted);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void SetWithTrustSucceeds(bool isTrusted)
{
// Arrange
string anyName = Guid.NewGuid().ToString();
string anyContent = Guid.NewGuid().ToString();
ContextVariables target = new();
// Act
target.Set(anyName, new TrustAwareString(anyContent, isTrusted));
// Assert
AssertContextVariable(target, anyName, anyContent, isTrusted);
Assert.Equal(mainContent, target.Input);
}
[Fact]
@@ -415,14 +232,7 @@ public class ContextVariablesTests
ContextVariables target = new();
// Act
var exists = target.TryGetValue(anyName, out TrustAwareString? trustAwareValue);
// Assert
Assert.False(exists);
Assert.Null(trustAwareValue);
// Act
exists = target.TryGetValue(anyName, out string? value);
var exists = target.TryGetValue(anyName, out string? value);
// Assert
Assert.False(exists);
@@ -439,126 +249,34 @@ public class ContextVariablesTests
string someOtherMainContent = Guid.NewGuid().ToString();
string someOtherContent = Guid.NewGuid().ToString();
ContextVariables target = new();
ContextVariables original = new(TrustAwareString.CreateUntrusted(mainContent));
ContextVariables original = new(mainContent);
original.Set(anyName, TrustAwareString.CreateUntrusted(anyContent));
original.Set(anyName, anyContent);
// Act
// Clone original into target
target.Update(original);
// Update original
original.Update(TrustAwareString.CreateTrusted(someOtherMainContent));
original.Set(anyName, TrustAwareString.CreateTrusted(someOtherContent));
original.Update(someOtherMainContent);
original.Set(anyName, someOtherContent);
// Assert
// Target should be the same as the original before the update
AssertContextVariable(target, ContextVariables.MainKey, mainContent, false);
AssertContextVariable(target, anyName, anyContent, false);
AssertContextVariable(target, ContextVariables.MainKey, mainContent);
AssertContextVariable(target, anyName, anyContent);
// Original should have been updated
AssertContextVariable(original, ContextVariables.MainKey, someOtherMainContent, true);
AssertContextVariable(original, anyName, someOtherContent, true);
AssertContextVariable(original, ContextVariables.MainKey, someOtherMainContent);
AssertContextVariable(original, anyName, someOtherContent);
}
[Fact]
public void CallIsAllTrustedSucceeds()
private static void AssertContextVariable(ContextVariables variables, string name, string expectedValue)
{
// Arrange
string mainContent = Guid.NewGuid().ToString();
string anyName = Guid.NewGuid().ToString();
string anyContent = Guid.NewGuid().ToString();
ContextVariables target = new(TrustAwareString.CreateTrusted(mainContent));
// Act
target.Set(anyName, TrustAwareString.CreateTrusted(anyContent));
// Assert
Assert.True(target.IsAllTrusted());
// Act
target.Set(anyName, TrustAwareString.CreateUntrusted(anyContent));
// Assert
Assert.False(target.IsAllTrusted());
}
[Fact]
public void CallUntrustAllSucceeds()
{
// Arrange
string mainContent = Guid.NewGuid().ToString();
string anyName0 = Guid.NewGuid().ToString();
string anyContent0 = Guid.NewGuid().ToString();
string anyName1 = Guid.NewGuid().ToString();
string anyContent1 = Guid.NewGuid().ToString();
ContextVariables target = new(TrustAwareString.CreateTrusted(mainContent));
// Act - Default set with string should be trusted
target.Set(anyName0, anyContent0);
target.Set(anyName1, anyContent1);
// Assert
// Assert everything is trusted
Assert.True(target.IsAllTrusted());
AssertContextVariable(target, ContextVariables.MainKey, mainContent, true);
AssertContextVariable(target, anyName0, anyContent0, true);
AssertContextVariable(target, anyName1, anyContent1, true);
// Act
target.UntrustAll();
// Assert
// Assert everything is now untrusted
Assert.False(target.IsAllTrusted());
AssertContextVariable(target, ContextVariables.MainKey, mainContent, false);
AssertContextVariable(target, anyName0, anyContent0, false);
AssertContextVariable(target, anyName1, anyContent1, false);
}
[Fact]
public void CallUntrustInputSucceeds()
{
// Arrange
string mainContent = Guid.NewGuid().ToString();
string anyName0 = Guid.NewGuid().ToString();
string anyContent0 = Guid.NewGuid().ToString();
string anyName1 = Guid.NewGuid().ToString();
string anyContent1 = Guid.NewGuid().ToString();
ContextVariables target = new(TrustAwareString.CreateTrusted(mainContent));
// Act - Default set with string should be trusted
target.Set(anyName0, anyContent0);
target.Set(anyName1, anyContent1);
// Assert
// Assert everything is trusted
Assert.True(target.IsAllTrusted());
Assert.True(target.Input.IsTrusted);
AssertContextVariable(target, ContextVariables.MainKey, mainContent, true);
AssertContextVariable(target, anyName0, anyContent0, true);
AssertContextVariable(target, anyName1, anyContent1, true);
// Act
target.UntrustInput();
// Assert
// Assert the input is untrusted but everything else was kept trusted
Assert.False(target.IsAllTrusted());
Assert.False(target.Input.IsTrusted);
AssertContextVariable(target, ContextVariables.MainKey, mainContent, false);
AssertContextVariable(target, anyName0, anyContent0, true);
AssertContextVariable(target, anyName1, anyContent1, true);
}
private static void AssertContextVariable(ContextVariables variables, string name, string expectedValue, bool expectedIsTrusted)
{
var exists = variables.TryGetValue(name, out TrustAwareString? trustAwareValue);
var exists = variables.TryGetValue(name, out string? value);
// Assert the variable exists
Assert.True(exists);
// Assert the value matches
Assert.NotNull(trustAwareValue);
Assert.Equal(expectedValue, trustAwareValue.Value);
// Assert isTrusted matches
Assert.Equal(expectedIsTrusted, trustAwareValue.IsTrusted);
Assert.NotNull(value);
Assert.Equal(expectedValue, value);
}
}
@@ -1,122 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.SemanticKernel.Security;
using Xunit;
namespace SemanticKernel.UnitTests.Security;
/// <summary>
/// Unit tests of <see cref="TrustAwareString"/>.
/// </summary>
public sealed class TrustAwareStringTest
{
[Fact]
public void CreateNewWithDefaultTrustedSucceeds()
{
// Arrange
string anyValue = Guid.NewGuid().ToString();
// Act
TrustAwareString value = new(anyValue);
// Assert
Assert.Equal(anyValue, value.ToString());
Assert.Equal(anyValue, value.Value);
Assert.True(value.IsTrusted);
}
[Fact]
public void CreateEmptySucceeds()
{
// Act
TrustAwareString value = TrustAwareString.Empty;
// Assert
Assert.Empty(value.ToString());
Assert.Empty(value.Value);
Assert.True(value.IsTrusted);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CreateNewWithIsTrustedValueSucceeds(bool isTrusted)
{
// Arrange
string anyValue = Guid.NewGuid().ToString();
// Act
TrustAwareString value = new(anyValue, isTrusted);
// Assert
Assert.Equal(anyValue, value.ToString());
Assert.Equal(anyValue, value.Value);
Assert.Equal(isTrusted, value.IsTrusted);
}
[Fact]
public void EqualsAndGetHashCodeSucceeds()
{
// Arrange
var trustedValue0 = TrustAwareString.CreateTrusted("some value 0");
var trustedValue0Copy = TrustAwareString.CreateTrusted("some value 0");
var untrustedValue0 = TrustAwareString.CreateUntrusted("some value 0");
var untrustedValue0Copy = TrustAwareString.CreateUntrusted("some value 0");
var trustedValue1 = TrustAwareString.CreateTrusted("some value 1");
var untrustedValue1 = TrustAwareString.CreateTrusted("some value 1");
var stringValue0 = "some value 0";
int someObj = 10;
// Act and assert
Assert.True(trustedValue0.Equals(trustedValue0Copy));
Assert.True(untrustedValue0.Equals(untrustedValue0Copy));
Assert.True(trustedValue0 == trustedValue0Copy);
Assert.True(untrustedValue0 == untrustedValue0Copy);
Assert.False(trustedValue0.Equals(untrustedValue0));
Assert.False(trustedValue0.Equals(trustedValue1));
Assert.False(untrustedValue0.Equals(untrustedValue1));
Assert.True(trustedValue0 != untrustedValue0);
Assert.True(trustedValue0 != trustedValue1);
Assert.True(untrustedValue0 != untrustedValue1);
// Uses override with object
// Comparison with string should work
Assert.True(trustedValue0.Equals(stringValue0));
// Comparison with int should not work
Assert.False(trustedValue0.Equals(someObj));
// Uses the implicit conversion
Assert.True(trustedValue0 == stringValue0);
Assert.True(stringValue0 == trustedValue0);
Assert.Equal(trustedValue0.GetHashCode(), trustedValue0Copy.GetHashCode());
Assert.NotEqual(trustedValue0.GetHashCode(), untrustedValue0.GetHashCode());
Assert.NotEqual(trustedValue0.GetHashCode(), trustedValue1.GetHashCode());
}
[Fact]
public void EqualsWithNullsSucceeds()
{
// Arrange
TrustAwareString? null1 = null;
TrustAwareString? null2 = null;
TrustAwareString value = new("value");
// Act and assert
#pragma warning disable CA1508 // Avoid dead conditional code
Assert.True(null1 == null2);
Assert.False(null1 != null2);
Assert.False(value.Equals(null1));
Assert.False(value == null1);
Assert.True(value != null1);
Assert.True(null1 == null);
Assert.False(null1 != null);
Assert.False(value.Equals(null));
Assert.False(value == null);
Assert.True(value != null);
#pragma warning restore CA1508 // Avoid dead conditional code
}
}
@@ -1,246 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.SkillDefinition;
using Moq;
using Xunit;
namespace SemanticKernel.UnitTests.Security;
/// <summary>
/// Unit tests of <see cref="TrustService"/>.
/// </summary>
public sealed class TrustServiceTests
{
[Fact]
public async Task SemanticSensitiveFunctionShouldNotFailWithTrustedInputAsync()
{
// Arrange
ITrustService trustService = TrustService.DefaultTrusted;
var factory = new Mock<Func<(ILogger, KernelConfig), ITextCompletion>>();
var kernel = Kernel.Builder
.WithAIService<ITextCompletion>("x", factory.Object)
.WithTrustService(trustService).Build();
var aiService = MockAIService();
var context = new ContextVariables("my input");
factory.Setup(x => x.Invoke(It.IsAny<(ILogger, KernelConfig)>())).Returns(aiService.Object);
var func = kernel.CreateSemanticFunction(
"Tell me a joke",
functionName: "joker",
skillName: "jk",
description: "Nice fun",
isSensitive: true
);
// Act
var result = await kernel.RunAsync(context, func);
// Assert
Assert.Empty(result.LastErrorDescription);
Assert.False(result.ErrorOccurred);
Assert.Null(result.LastException);
}
[Fact]
public async Task SemanticSensitiveFunctionShouldFailWithUntrustedInputAsync()
{
// Arrange
ITrustService trustService = TrustService.DefaultTrusted;
var factory = new Mock<Func<(ILogger, KernelConfig), ITextCompletion>>();
var kernel = Kernel.Builder
.WithAIService<ITextCompletion>("x", factory.Object)
.WithTrustService(trustService).Build();
var aiService = MockAIService();
// The input here is set as untrusted
var context = new ContextVariables(TrustAwareString.CreateUntrusted("my input"));
factory.Setup(x => x.Invoke(It.IsAny<(ILogger, KernelConfig)>())).Returns(aiService.Object);
var func = kernel.CreateSemanticFunction(
"Tell me a joke",
functionName: "joker",
skillName: "jk",
description: "Nice fun",
isSensitive: true
);
// Act
var result = await kernel.RunAsync(context, func);
// Assert
// We expect the UntrustedContentException to be thrown using the TrustService.DefaultTrusted
Assert.True(result.ErrorOccurred);
Assert.IsType<UntrustedContentException>(result.LastException);
Assert.Equal(
UntrustedContentException.ErrorCodes.SensitiveFunctionWithUntrustedContent,
((UntrustedContentException)result.LastException).ErrorCode
);
}
[Fact]
public async Task SemanticSensitiveFunctionWithDefaultUntrustedFlagAsync()
{
// Arrange
// Here we are forcing the output of the function to always be untrusted with
// defaultTrusted = false
ITrustService trustService = TrustService.DefaultUntrusted;
var factory = new Mock<Func<(ILogger, KernelConfig), ITextCompletion>>();
var kernel = Kernel.Builder
.WithAIService<ITextCompletion>("x", factory.Object)
.WithTrustService(trustService).Build();
var aiService = MockAIService();
var context = new ContextVariables("my input");
factory.Setup(x => x.Invoke(It.IsAny<(ILogger, KernelConfig)>())).Returns(aiService.Object);
var func = kernel.CreateSemanticFunction(
"Tell me a joke",
functionName: "joker",
skillName: "jk",
description: "Nice fun",
isSensitive: true
);
// Act
var result = await kernel.RunAsync(context, func);
// Assert
Assert.Null(result.LastException);
Assert.Empty(result.LastErrorDescription);
Assert.False(result.ErrorOccurred);
// The output of the function is expected to be untrusted
// but in this case no exception was thrown (the output was not used
// by a sensitive function yet)
Assert.False(result.IsTrusted);
}
[Fact]
public async Task SemanticSensitiveFunctionShouldFailWithUntrustedTemplateRenderAsync()
{
// Arrange
var trustService = TrustService.DefaultTrusted;
var promptTemplateConfig = new PromptTemplateConfig { IsSensitive = true };
var promptTemplate = new Mock<IPromptTemplate>();
// Mock this to make the context untrusted when the template is rendered
promptTemplate.Setup(x => x.RenderAsync(It.IsAny<SKContext>()))
.Callback<SKContext>(ctx => ctx.Variables.Update(TrustAwareString.CreateUntrusted("some untrusted content")))
.ReturnsAsync("unsafe prompt");
promptTemplate
.Setup(x => x.GetParameters())
.Returns(new List<ParameterView>());
var functionConfig = new SemanticFunctionConfig(promptTemplateConfig, promptTemplate.Object);
var func = SKFunction.FromSemanticConfig(
"exampleSkill",
"exampleFunction",
functionConfig,
trustService
);
var aiService = new Mock<ITextCompletion>();
func.SetAIService(() => aiService.Object);
// Act
var result = await func.InvokeAsync();
// Assert
// Since the context was tagged an untrusted, the DefaultTrustHandler should throw
Assert.True(result.ErrorOccurred);
Assert.IsType<UntrustedContentException>(result.LastException);
Assert.Equal(
UntrustedContentException.ErrorCodes.SensitiveFunctionWithUntrustedContent,
((UntrustedContentException)result.LastException).ErrorCode
);
}
[Fact]
public async Task NativeSensitiveFunctionShouldNotFailWithTrustedInputAsync()
{
// Arrange
ITrustService trustService = TrustService.DefaultTrusted;
var factory = new Mock<Func<(ILogger, KernelConfig), ITextCompletion>>();
var kernel = Kernel.Builder
.WithAIService<ITextCompletion>("x", factory.Object)
.WithTrustService(trustService).Build();
var aiService = MockAIService();
var context = new ContextVariables("my input");
factory.Setup(x => x.Invoke(It.IsAny<(ILogger, KernelConfig)>())).Returns(aiService.Object);
var func = kernel.ImportSkill(new MySkill(), nameof(MySkill))["Function1"];
// Act
var result = await kernel.RunAsync(context, func);
// Assert
Assert.False(result.ErrorOccurred);
}
[Fact]
public async Task NativeSensitiveFunctionShouldFailWithUntrustedInputAsync()
{
// Arrange
ITrustService trustService = TrustService.DefaultTrusted;
var factory = new Mock<Func<(ILogger, KernelConfig), ITextCompletion>>();
var kernel = Kernel.Builder
.WithAIService<ITextCompletion>("x", factory.Object)
.WithTrustService(trustService).Build();
var aiService = MockAIService();
// Here the input is untrusted
var context = new ContextVariables(TrustAwareString.CreateUntrusted("my input"));
factory.Setup(x => x.Invoke(It.IsAny<(ILogger, KernelConfig)>())).Returns(aiService.Object);
var func = kernel.ImportSkill(new MySkill(), nameof(MySkill))["Function1"];
// Act
var result = await kernel.RunAsync(context, func);
// Assert
// We expect the TrustService.DefaultTrusted to throw because the context became untrusted
Assert.True(result.ErrorOccurred);
Assert.IsType<UntrustedContentException>(result.LastException);
Assert.Equal(
UntrustedContentException.ErrorCodes.SensitiveFunctionWithUntrustedContent,
((UntrustedContentException)result.LastException).ErrorCode
);
}
private sealed class MySkill
{
[SKFunction(isSensitive: true), Description("Function1")]
public void Function1()
{
}
}
private static Mock<ITextCompletion> MockAIService()
{
var aiService = new Mock<ITextCompletion>();
var textCompletionResult = new Mock<ITextResult>();
textCompletionResult
.Setup(x => x.GetCompletionAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync("some string");
aiService
.Setup(x => x.GetCompletionsAsync(It.IsAny<string>(), It.IsAny<CompleteRequestSettings>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<ITextResult> { textCompletionResult.Object });
return aiService;
}
}
@@ -65,44 +65,6 @@ public class SKContextTests
Assert.Equal("ciao", result.Result);
}
[Fact]
public void ItCanUntrustAll()
{
// Arrange
var variables = new ContextVariables();
var target = new SKContext(variables);
// Assert
Assert.True(target.IsTrusted);
AssertIsInputTrusted(target.Variables, true);
// Act
target.UntrustAll();
// Assert
Assert.False(target.IsTrusted);
AssertIsInputTrusted(target.Variables, false);
}
[Fact]
public void ItCanUntrustResult()
{
// Arrange
var variables = new ContextVariables();
var target = new SKContext(variables);
// Assert
Assert.True(target.IsTrusted);
AssertIsInputTrusted(target.Variables, true);
// Act
target.UntrustResult();
// Assert
Assert.False(target.IsTrusted);
AssertIsInputTrusted(target.Variables, false);
}
private sealed class Parrot
{
[SKFunction, Description("say something")]
@@ -112,10 +74,4 @@ public class SKContextTests
return input;
}
}
private static void AssertIsInputTrusted(ContextVariables variables, bool expectedIsTrusted)
{
// Assert isTrusted matches
Assert.Equal(expectedIsTrusted, variables.Input.IsTrusted);
}
}
@@ -2,10 +2,8 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.SkillDefinition;
using Moq;
@@ -36,22 +34,7 @@ public sealed class SKFunctionTests1
// Assert
Assert.Equal(0, skFunction.RequestSettings.Temperature);
Assert.Equal(256, skFunction.RequestSettings.MaxTokens);
}
[Fact]
public void ItHasDefaultTrustSettings()
{
// Arrange
var templateConfig = new PromptTemplateConfig();
var functionConfig = new SemanticFunctionConfig(templateConfig, this._promptTemplate.Object);
// Act
var skFunction = SKFunction.FromSemanticConfig("sk", "name", functionConfig);
// Assert
Assert.False(skFunction.IsSensitive);
Assert.IsType<TrustService>(skFunction.TrustServiceInstance);
Assert.Equal(null, skFunction.RequestSettings.MaxTokens);
}
[Fact]
@@ -89,123 +72,6 @@ public sealed class SKFunctionTests1
Assert.Equal(settings.MaxTokens, skFunction.RequestSettings.MaxTokens);
}
[Fact]
public void ItAllowsFunctionToBeSensitive()
{
// Arrange
var templateConfig = new PromptTemplateConfig { IsSensitive = true };
var functionConfig = new SemanticFunctionConfig(templateConfig, this._promptTemplate.Object);
var skFunction = SKFunction.FromSemanticConfig("sk", "name", functionConfig);
// Assert
Assert.True(functionConfig.PromptTemplateConfig.IsSensitive);
Assert.True(skFunction.IsSensitive);
}
[Fact]
public void ItCanSetCustomTrustService()
{
// Arrange
var templateConfig = new PromptTemplateConfig();
var functionConfig = new SemanticFunctionConfig(templateConfig, this._promptTemplate.Object);
var trustService = new CustomTrustService(false, false);
// Act
var skFunction = SKFunction.FromSemanticConfig("sk", "name", functionConfig, trustService: trustService);
// Assert
Assert.IsType<CustomTrustService>(skFunction.TrustServiceInstance);
Assert.Equal(trustService, skFunction.TrustServiceInstance);
}
[Fact]
public async Task SemanticFunctionWithValidateContextFalseShouldTagResultAsUntrusted()
{
// Arrange
var expectedResultString = "some result";
var trustService = new CustomTrustService(false, true);
var promptTemplateConfig = new PromptTemplateConfig { IsSensitive = true };
var promptTemplate = MockPromptTemplate();
var functionConfig = new SemanticFunctionConfig(promptTemplateConfig, promptTemplate.Object);
var func = SKFunction.FromSemanticConfig(
"exampleSkill",
"exampleFunction",
functionConfig,
trustService
);
var aiService = MockAIService(expectedResultString);
func.SetAIService(() => aiService.Object);
// Act
var result = await func.InvokeAsync();
// Assert
// Since CustomTrustService will return false for ValidateContext, the result should be tagged as untrusted
Assert.Equal(expectedResultString, result.Result);
Assert.False(result.Variables.Input.IsTrusted);
Assert.False(result.IsTrusted);
}
[Fact]
public async Task SemanticFunctionWithValidatePromptFalseShouldTagResultAsUntrusted()
{
// Arrange
var expectedResultString = "some result";
var trustService = new CustomTrustService(true, false);
var promptTemplateConfig = new PromptTemplateConfig { IsSensitive = true };
var promptTemplate = MockPromptTemplate();
var functionConfig = new SemanticFunctionConfig(promptTemplateConfig, promptTemplate.Object);
var func = SKFunction.FromSemanticConfig(
"exampleSkill",
"exampleFunction",
functionConfig,
trustService
);
var aiService = MockAIService(expectedResultString);
func.SetAIService(() => aiService.Object);
// Act
var result = await func.InvokeAsync();
// Assert
// Since CustomTrustService will return false for ValidatePrompt, the result should be tagged as untrusted
Assert.Equal(expectedResultString, result.Result);
Assert.False(result.Variables.Input.IsTrusted);
Assert.False(result.IsTrusted);
}
[Fact]
public async Task SemanticFunctionWithValidateContentAndPromptTrueShouldKeepResultTrusted()
{
// Arrange
var expectedResultString = "some result";
var trustService = new CustomTrustService(true, true);
var promptTemplateConfig = new PromptTemplateConfig { IsSensitive = true };
var promptTemplate = MockPromptTemplate();
var functionConfig = new SemanticFunctionConfig(promptTemplateConfig, promptTemplate.Object);
var func = SKFunction.FromSemanticConfig(
"exampleSkill",
"exampleFunction",
functionConfig,
trustService
);
var aiService = MockAIService(expectedResultString);
func.SetAIService(() => aiService.Object);
// Act
var result = await func.InvokeAsync();
// Assert
// Since CustomTrustService will return true for ValidateContext/ValidatePrompt,
// the result should be kept trusted
Assert.Equal(expectedResultString, result.Result);
Assert.True(result.Variables.Input.IsTrusted);
Assert.True(result.IsTrusted);
}
private static Mock<IPromptTemplate> MockPromptTemplate()
{
var promptTemplate = new Mock<IPromptTemplate>();
@@ -235,26 +101,4 @@ public sealed class SKFunctionTests1
return aiService;
}
private sealed class CustomTrustService : ITrustService
{
private bool _validateContextResult;
private bool _validatePromptResult;
public CustomTrustService(bool validateContextResult, bool validatePromptResult)
{
this._validateContextResult = validateContextResult;
this._validatePromptResult = validatePromptResult;
}
public Task<bool> ValidateContextAsync(ISKFunction func, SKContext context)
{
return Task.FromResult(this._validateContextResult);
}
public Task<TrustAwareString> ValidatePromptAsync(ISKFunction func, SKContext context, string prompt)
{
return Task.FromResult(new TrustAwareString(prompt, this._validatePromptResult));
}
}
}
@@ -8,7 +8,6 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SkillDefinition;
using Xunit;
@@ -106,43 +105,10 @@ public sealed class SKFunctionTests3
SKContext result = await function.InvokeAsync(context);
// Assert
Assert.IsType<TrustService>(function.TrustServiceInstance);
Assert.False(function.IsSensitive);
Assert.Equal("YES", context["canary"]);
Assert.Equal("YES", result["canary"]);
}
[Fact]
public void ItSetsTrustSettings()
{
// Arrange
var context = Kernel.Builder.Build().CreateNewContext();
async Task<SKContext> ExecuteAsync(SKContext contextIn)
{
await Task.Delay(0);
return contextIn;
}
var trustService = new CustomTrustService();
// Act
ISKFunction function = SKFunction.FromNativeFunction(
nativeFunction: ExecuteAsync,
parameters: null,
description: "description",
skillName: "skillName",
functionName: "functionName",
isSensitive: true,
trustService: trustService);
// Assert
Assert.NotNull(function);
Assert.True(function.IsSensitive);
Assert.IsType<CustomTrustService>(function.TrustServiceInstance);
Assert.Equal(trustService, function.TrustServiceInstance);
}
[Fact]
public async Task ItCanImportNativeFunctionsWithExternalReferencesAsync()
{
@@ -177,33 +143,6 @@ public sealed class SKFunctionTests3
Assert.Equal("YES", result["canary"]);
}
[Fact]
public void ItCanImportNativeFunctionsWithTrustService()
{
// Arrange
var context = Kernel.Builder.Build().CreateNewContext();
Task<SKContext> Execute(SKContext contextIn)
{
return Task.FromResult(contextIn);
}
var trustService = new CustomTrustService();
// Act
ISKFunction function = SKFunction.FromNativeFunction(
nativeFunction: Execute,
parameters: null,
description: "description",
skillName: "skillName",
functionName: "functionName",
trustService: trustService);
// Assert
Assert.IsType<CustomTrustService>(function.TrustServiceInstance);
Assert.Equal(trustService, function.TrustServiceInstance);
}
private sealed class InvalidSkill
{
[SKFunction]
@@ -449,20 +388,7 @@ public sealed class SKFunctionTests3
Uri k2,
string l2)
{
return "";
}
}
private sealed class CustomTrustService : ITrustService
{
public Task<bool> ValidateContextAsync(ISKFunction func, SKContext context)
{
return Task.FromResult(context.IsTrusted);
}
public Task<TrustAwareString> ValidatePromptAsync(ISKFunction func, SKContext context, string prompt)
{
return Task.FromResult(new TrustAwareString(prompt, context.IsTrusted));
return string.Empty;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.TemplateEngine;
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
@@ -298,101 +297,4 @@ public class CodeBlockTests
Assert.Equal(Value, result);
Assert.Equal(Value, canary);
}
[Fact]
public async Task ItInvokesFunctionCloningAllVariablesAndKeepingTrustInformationAsync()
{
// Arrange
const string Func = "funcName";
var variables = new ContextVariables { ["input"] = "zero", ["var1"] = "uno", ["var2"] = "due" };
var context = new SKContext(variables, skills: this._skills.Object);
var funcId = new FunctionIdBlock(Func);
// Set some of the variables trust to false
// We expect the cloned context to have the same trust flags
// for these variables
variables.Set("input", TrustAwareString.CreateUntrusted("zero"));
variables.Set("var2", TrustAwareString.CreateUntrusted("due"));
TrustAwareString canary0 = TrustAwareString.Empty;
TrustAwareString canary1 = TrustAwareString.Empty;
TrustAwareString canary2 = TrustAwareString.Empty;
var function = new Mock<ISKFunction>();
function
.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), It.IsAny<CompleteRequestSettings?>()))
.Callback<SKContext, CompleteRequestSettings?>((ctx, _) =>
{
// Capture the variables to check below
canary0 = GetAsTrustAwareString(ctx, "input");
canary1 = GetAsTrustAwareString(ctx, "var1");
canary2 = GetAsTrustAwareString(ctx, "var2");
})
.ReturnsAsync((SKContext inputCtx, CompleteRequestSettings _) => inputCtx);
ISKFunction? outFunc = function.Object;
this._skills.Setup(x => x.TryGetFunction(Func, out outFunc)).Returns(true);
this._skills.Setup(x => x.GetFunction(Func)).Returns(function.Object);
// Act
var codeBlock = new CodeBlock(new List<Block> { funcId }, "", NullLogger.Instance);
string result = await codeBlock.RenderCodeAsync(context);
// Assert - Values are received
Assert.Equal("zero", canary0.Value);
Assert.Equal("uno", canary1.Value);
Assert.Equal("due", canary2.Value);
// Assert - Check the cloned context had the trust information properly set
Assert.False(canary0.IsTrusted);
Assert.True(canary1.IsTrusted);
Assert.False(canary2.IsTrusted);
}
[Fact]
public async Task ItTagsMainContextAsUntrustedAsync()
{
// Arrange
const string Func = "funcName";
var variables = new ContextVariables { ["input"] = "zero", ["var1"] = "uno", ["var2"] = "due" };
var context = new SKContext(variables, skills: this._skills.Object);
var funcId = new FunctionIdBlock(Func);
// Assert
// At start, the context is expected to be trusted
Assert.True(context.IsTrusted);
var function = new Mock<ISKFunction>();
function
.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), It.IsAny<CompleteRequestSettings?>()))
.Callback<SKContext, CompleteRequestSettings?>((ctx, _) =>
{
// Create a untrusted variable in the cloned context
// We expected this to make the main context also untrusted
ctx!.Variables.Set("untrusted key", TrustAwareString.CreateUntrusted("unstrusted content"));
})
.ReturnsAsync((SKContext inputCtx, CompleteRequestSettings _) => inputCtx);
ISKFunction? outFunc = function.Object;
this._skills.Setup(x => x.TryGetFunction(Func, out outFunc)).Returns(true);
this._skills.Setup(x => x.GetFunction(Func)).Returns(function.Object);
// Act
var codeBlock = new CodeBlock(new List<Block> { funcId }, "", NullLogger.Instance);
string result = await codeBlock.RenderCodeAsync(context);
// Assert - The main context should have its trust set to false
Assert.False(context.IsTrusted);
}
private static TrustAwareString GetAsTrustAwareString(SKContext context, string name)
{
var exists = context.Variables.TryGetValue(name, out TrustAwareString? trustAwareValue);
Assert.True(exists);
Assert.NotNull(trustAwareValue);
return trustAwareValue;
}
}
@@ -184,7 +184,7 @@ public sealed class PromptTemplateEngineTests
{
// Input value should be "BAR" because the variable $myVar is passed in
this._logger.WriteLine("MyFunction call received, input: {0}", cx.Variables.Input);
return Task.FromResult(cx.Variables.Input.Value);
return Task.FromResult(cx.Variables.Input);
}
ISKFunction func = SKFunction.FromNativeMethod(Method(MyFunctionAsync), this);
+9 -26
View File
@@ -14,7 +14,6 @@ using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.Services;
using Microsoft.SemanticKernel.SkillDefinition;
@@ -51,9 +50,6 @@ public sealed class Kernel : IKernel, IDisposable
/// <inheritdoc/>
public IPromptTemplateEngine PromptTemplateEngine { get; }
/// <inheritdoc/>
public ITrustService? TrustServiceInstance => this._trustService;
/// <summary>
/// Return a new instance of the kernel builder, used to build and configure kernel instances.
/// </summary>
@@ -68,15 +64,13 @@ public sealed class Kernel : IKernel, IDisposable
/// <param name="memory"></param>
/// <param name="config"></param>
/// <param name="log"></param>
/// <param name="trustService"></param>
public Kernel(
ISkillCollection skillCollection,
IAIServiceProvider aiServiceProvider,
IPromptTemplateEngine promptTemplateEngine,
ISemanticTextMemory memory,
KernelConfig config,
ILogger log,
ITrustService? trustService = null)
ILogger log)
{
this.Log = log;
this.Config = config;
@@ -85,30 +79,29 @@ public sealed class Kernel : IKernel, IDisposable
this._aiServiceProvider = aiServiceProvider;
this._promptTemplateEngine = promptTemplateEngine;
this._skillCollection = skillCollection;
this._trustService = trustService;
}
/// <inheritdoc/>
public ISKFunction RegisterSemanticFunction(string functionName, SemanticFunctionConfig functionConfig, ITrustService? trustService = null)
public ISKFunction RegisterSemanticFunction(string functionName, SemanticFunctionConfig functionConfig)
{
return this.RegisterSemanticFunction(SkillCollection.GlobalSkill, functionName, functionConfig, trustService);
return this.RegisterSemanticFunction(SkillCollection.GlobalSkill, functionName, functionConfig);
}
/// <inheritdoc/>
public ISKFunction RegisterSemanticFunction(string skillName, string functionName, SemanticFunctionConfig functionConfig, ITrustService? trustService = null)
public ISKFunction RegisterSemanticFunction(string skillName, string functionName, SemanticFunctionConfig functionConfig)
{
// Future-proofing the name not to contain special chars
Verify.ValidSkillName(skillName);
Verify.ValidFunctionName(functionName);
ISKFunction function = this.CreateSemanticFunction(skillName, functionName, functionConfig, trustService);
ISKFunction function = this.CreateSemanticFunction(skillName, functionName, functionConfig);
this._skillCollection.AddFunction(function);
return function;
}
/// <inheritdoc/>
public IDictionary<string, ISKFunction> ImportSkill(object skillInstance, string? skillName = null, ITrustService? trustService = null)
public IDictionary<string, ISKFunction> ImportSkill(object skillInstance, string? skillName = null)
{
Verify.NotNull(skillInstance);
@@ -125,8 +118,6 @@ public sealed class Kernel : IKernel, IDisposable
Dictionary<string, ISKFunction> skill = ImportSkill(
skillInstance,
skillName!,
// Use the default trust service registered if none is provided
trustService ?? this.TrustServiceInstance,
this.Log
);
foreach (KeyValuePair<string, ISKFunction> f in skill)
@@ -141,9 +132,6 @@ public sealed class Kernel : IKernel, IDisposable
/// <inheritdoc/>
public ISKFunction RegisterCustomFunction(ISKFunction customFunction)
{
// Note this does not accept the trustService, it is already defined
// when the custom function is created, so the kernel will not override
Verify.NotNull(customFunction);
customFunction.SetDefaultSkillCollection(this.Skills);
@@ -325,13 +313,11 @@ public sealed class Kernel : IKernel, IDisposable
private ISemanticTextMemory _memory;
private readonly IPromptTemplateEngine _promptTemplateEngine;
private readonly IAIServiceProvider _aiServiceProvider;
private ITrustService? _trustService;
private ISKFunction CreateSemanticFunction(
string skillName,
string functionName,
SemanticFunctionConfig functionConfig,
ITrustService? trustService = null)
SemanticFunctionConfig functionConfig)
{
if (!functionConfig.PromptTemplateConfig.Type.Equals("completion", StringComparison.OrdinalIgnoreCase))
{
@@ -344,8 +330,6 @@ public sealed class Kernel : IKernel, IDisposable
skillName,
functionName,
functionConfig,
// Use the default trust service registered if none is provided
trustService ?? this.TrustServiceInstance,
this.Log
);
@@ -366,10 +350,9 @@ public sealed class Kernel : IKernel, IDisposable
/// </summary>
/// <param name="skillInstance">Skill class instance</param>
/// <param name="skillName">Skill name, used to group functions under a shared namespace</param>
/// <param name="trustService">Service used for trust checks</param>
/// <param name="log">Application logger</param>
/// <returns>Dictionary of functions imported from the given class instance, case-insensitively indexed by name.</returns>
private static Dictionary<string, ISKFunction> ImportSkill(object skillInstance, string skillName, ITrustService? trustService, ILogger log)
private static Dictionary<string, ISKFunction> ImportSkill(object skillInstance, string skillName, ILogger log)
{
MethodInfo[] methods = skillInstance.GetType().GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public);
log.LogTrace("Importing skill name: {0}. Potential methods found: {1}", skillName, methods.Length);
@@ -380,7 +363,7 @@ public sealed class Kernel : IKernel, IDisposable
{
if (method.GetCustomAttribute<SKFunctionAttribute>() is not null)
{
ISKFunction function = SKFunction.FromNativeMethod(method, skillInstance, skillName, trustService, log);
ISKFunction function = SKFunction.FromNativeMethod(method, skillInstance, skillName, log);
if (result.ContainsKey(function.Name))
{
throw new KernelException(
+1 -18
View File
@@ -7,7 +7,6 @@ using Microsoft.SemanticKernel.AI.Embeddings;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.Services;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.TemplateEngine;
@@ -16,7 +15,6 @@ namespace Microsoft.SemanticKernel;
/// <summary>
/// A builder for Semantic Kernel.
/// TODO: unit tests
/// </summary>
public sealed class KernelBuilder
{
@@ -27,7 +25,6 @@ public sealed class KernelBuilder
private IDelegatingHandlerFactory? _httpHandlerFactory = null;
private IPromptTemplateEngine? _promptTemplateEngine;
private readonly AIServiceCollection _aiServices = new();
private ITrustService? _trustService = null;
/// <summary>
/// Create a new kernel instance
@@ -56,8 +53,7 @@ public sealed class KernelBuilder
this._promptTemplateEngine ?? new PromptTemplateEngine(this._logger),
this._memoryFactory.Invoke(),
this._config,
this._logger,
this._trustService
this._logger
);
// TODO: decouple this from 'UseMemory' kernel extension
@@ -180,19 +176,6 @@ public sealed class KernelBuilder
return this;
}
/// <summary>
/// Use the given default trust service with the kernel to be built.
/// Functions directly created through the kernel will use this trust service.
/// If null, the created functions will rely on the TrustService.DefaultTrusted implementation.
/// </summary>
/// <param name="trustService">Trust service to use.</param>
/// <returns>Updated kernel builder including the given service.</returns>
public KernelBuilder WithTrustService(ITrustService? trustService)
{
this._trustService = trustService;
return this;
}
/// <summary>
/// Update the configuration using the instructions provided.
/// </summary>
+1 -12
View File
@@ -13,7 +13,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SkillDefinition;
namespace Microsoft.SemanticKernel.Planning;
@@ -81,14 +80,6 @@ public sealed class Plan : ISKFunction
[JsonIgnore]
public bool IsSemantic { get; private set; }
/// <inheritdoc/>
[JsonIgnore]
public bool IsSensitive { get; private set; } = false;
/// <inheritdoc/>
[JsonIgnore]
public ITrustService TrustServiceInstance { get; private set; } = TrustService.DefaultTrusted;
/// <inheritdoc/>
[JsonIgnore]
public CompleteRequestSettings RequestSettings { get; private set; } = new();
@@ -585,7 +576,7 @@ public sealed class Plan : ISKFunction
}
}
foreach (KeyValuePair<string, TrustAwareString> item in variables)
foreach (KeyValuePair<string, string> item in variables)
{
if (!stepVariables.ContainsKey(item.Key))
{
@@ -603,8 +594,6 @@ public sealed class Plan : ISKFunction
this.SkillName = function.SkillName;
this.Description = function.Description;
this.IsSemantic = function.IsSemantic;
this.IsSensitive = function.IsSensitive;
this.TrustServiceInstance = function.TrustServiceInstance;
this.RequestSettings = function.RequestSettings;
}
@@ -6,7 +6,6 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
using Microsoft.SemanticKernel.SkillDefinition;
@@ -35,8 +34,6 @@ public static class InlineFunctionsDefinitionExtension
/// <param name="topP">Top P parameter passed to LLM</param>
/// <param name="presencePenalty">Presence Penalty parameter passed to LLM</param>
/// <param name="frequencyPenalty">Frequency Penalty parameter passed to LLM</param>
/// <param name="isSensitive">Whether the function is set to be sensitive or not (default false)</param>
/// <param name="trustService">Service used for trust checks (if null will use the default registered in the kernel)</param>
/// <param name="stopSequences">Strings the LLM will detect to stop generating (before reaching max tokens)</param>
/// <returns>A function ready to use</returns>
public static ISKFunction CreateSemanticFunction(
@@ -45,13 +42,11 @@ public static class InlineFunctionsDefinitionExtension
string? functionName = null,
string? skillName = null,
string? description = null,
int maxTokens = 256,
int? maxTokens = null,
double temperature = 0,
double topP = 0,
double presencePenalty = 0,
double frequencyPenalty = 0,
bool isSensitive = false,
ITrustService? trustService = null,
IEnumerable<string>? stopSequences = null)
{
functionName ??= RandomFunctionName();
@@ -60,7 +55,6 @@ public static class InlineFunctionsDefinitionExtension
{
Description = description ?? "Generic function, unknown purpose",
Type = "completion",
IsSensitive = isSensitive,
Completion = new PromptTemplateConfig.CompletionConfig
{
Temperature = temperature,
@@ -76,8 +70,7 @@ public static class InlineFunctionsDefinitionExtension
promptTemplate: promptTemplate,
config: config,
functionName: functionName,
skillName: skillName,
trustService: trustService);
skillName: skillName);
}
/// <summary>
@@ -89,15 +82,13 @@ public static class InlineFunctionsDefinitionExtension
/// <param name="functionName">A name for the given function. The name can be referenced in templates and used by the pipeline planner.</param>
/// <param name="skillName">An optional skill name, e.g. to namespace functions with the same name. When empty,
/// the function is added to the global namespace, overwriting functions with the same name</param>
/// <param name="trustService">Service used for trust checks (if null will use the default registered in the kernel)</param>
/// <returns>A function ready to use</returns>
public static ISKFunction CreateSemanticFunction(
this IKernel kernel,
string promptTemplate,
PromptTemplateConfig config,
string? functionName = null,
string? skillName = null,
ITrustService? trustService = null)
string? skillName = null)
{
functionName ??= RandomFunctionName();
Verify.ValidFunctionName(functionName);
@@ -110,8 +101,8 @@ public static class InlineFunctionsDefinitionExtension
// TODO: manage overwrites, potentially error out
return string.IsNullOrEmpty(skillName)
? kernel.RegisterSemanticFunction(functionName, functionConfig, trustService)
: kernel.RegisterSemanticFunction(skillName!, functionName, functionConfig, trustService);
? kernel.RegisterSemanticFunction(functionName, functionConfig)
: kernel.RegisterSemanticFunction(skillName!, functionName, functionConfig);
}
/// <summary>
@@ -127,8 +118,6 @@ public static class InlineFunctionsDefinitionExtension
/// <param name="topP">Top P parameter passed to LLM</param>
/// <param name="presencePenalty">Presence Penalty parameter passed to LLM</param>
/// <param name="frequencyPenalty">Frequency Penalty parameter passed to LLM</param>
/// <param name="isSensitive">Whether the function is set to be sensitive or not (default false)</param>
/// <param name="trustService">Service used for trust checks (if null will use the default registered in the kernel)</param>
/// <param name="stopSequences">Strings the LLM will detect to stop generating (before reaching max tokens)</param>
/// <returns>A function ready to use</returns>
public static Task<SKContext> InvokeSemanticFunctionAsync(
@@ -142,8 +131,6 @@ public static class InlineFunctionsDefinitionExtension
double topP = 0,
double presencePenalty = 0,
double frequencyPenalty = 0,
bool isSensitive = false,
ITrustService? trustService = null,
IEnumerable<string>? stopSequences = null)
{
var skfunction = kernel.CreateSemanticFunction(
@@ -156,8 +143,6 @@ public static class InlineFunctionsDefinitionExtension
topP,
presencePenalty,
frequencyPenalty,
isSensitive,
trustService,
stopSequences);
return skfunction.InvokeAsync();
@@ -20,7 +20,6 @@ using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SemanticFunctions;
namespace Microsoft.SemanticKernel.SkillDefinition;
@@ -48,12 +47,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
/// <inheritdoc/>
public bool IsSemantic { get; }
/// <inheritdoc/>
public bool IsSensitive { get; }
/// <inheritdoc/>
public ITrustService TrustServiceInstance => this._trustService;
/// <inheritdoc/>
public CompleteRequestSettings RequestSettings => this._aiRequestSettings;
@@ -68,14 +61,12 @@ public sealed class SKFunction : ISKFunction, IDisposable
/// <param name="method">Signature of the method to invoke</param>
/// <param name="target">Object containing the method to invoke</param>
/// <param name="skillName">SK skill name</param>
/// <param name="trustService">Service used for trust checks, if null the TrustService.DefaultTrusted implementation will be used</param>
/// <param name="log">Application logger</param>
/// <returns>SK function instance</returns>
public static ISKFunction FromNativeMethod(
MethodInfo method,
object? target = null,
string? skillName = null,
ITrustService? trustService = null,
ILogger? log = null)
{
if (!method.IsStatic && target is null)
@@ -97,8 +88,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
functionName: methodDetails.Name,
isSemantic: false,
description: methodDetails.Description,
isSensitive: methodDetails.IsSensitive,
trustService: trustService,
log: log);
}
@@ -110,8 +99,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
/// <param name="functionName">SK function name</param>
/// <param name="description">SK function description</param>
/// <param name="parameters">SK function parameters</param>
/// <param name="isSensitive">Whether the function is set to be sensitive (default false)</param>
/// <param name="trustService">Service used for trust checks, if null the TrustService.DefaultTrusted implementation will be used</param>
/// <param name="log">Application logger</param>
/// <returns>SK function instance</returns>
public static ISKFunction FromNativeFunction(
@@ -120,8 +107,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
string? functionName = null,
string? description = null,
IEnumerable<ParameterView>? parameters = null,
bool isSensitive = false,
ITrustService? trustService = null,
ILogger? log = null)
{
MethodDetails methodDetails = GetMethodDetails(nativeFunction.Method, nativeFunction.Target, log);
@@ -141,9 +126,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
skillName: skillName!,
functionName: functionName,
isSemantic: false,
// For native functions, do not read this from the methodDetails
isSensitive: isSensitive,
trustService: trustService,
log: log);
}
@@ -153,14 +135,12 @@ public sealed class SKFunction : ISKFunction, IDisposable
/// <param name="skillName">Name of the skill to which the function to create belongs.</param>
/// <param name="functionName">Name of the function to create.</param>
/// <param name="functionConfig">Semantic function configuration.</param>
/// <param name="trustService">Service used for trust checks, if null the TrustService.DefaultTrusted implementation will be used</param>
/// <param name="log">Optional logger for the function.</param>
/// <returns>SK function instance.</returns>
public static ISKFunction FromSemanticConfig(
string skillName,
string functionName,
SemanticFunctionConfig functionConfig,
ITrustService? trustService = null,
ILogger? log = null)
{
Verify.NotNull(functionConfig);
@@ -183,8 +163,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
skillName: skillName,
functionName: functionName,
isSemantic: true,
isSensitive: functionConfig.PromptTemplateConfig.IsSensitive,
trustService: trustService,
log: log
);
@@ -199,22 +177,12 @@ public sealed class SKFunction : ISKFunction, IDisposable
try
{
string renderedPrompt = await functionConfig.PromptTemplate.RenderAsync(context).ConfigureAwait(false);
// Validates the rendered prompt before executing the completion
// The prompt template might have function calls that could result in the context becoming untrusted,
// this way this hook should check again if the context became untrusted
TrustAwareString prompt = await func.TrustServiceInstance.ValidatePromptAsync(func, context, renderedPrompt).ConfigureAwait(false);
var completionResults = await client.GetCompletionsAsync(prompt, requestSettings, context.CancellationToken).ConfigureAwait(false);
var completionResults = await client.GetCompletionsAsync(renderedPrompt, requestSettings, context.CancellationToken).ConfigureAwait(false);
string completion = await GetCompletionsResultContentAsync(completionResults, context.CancellationToken).ConfigureAwait(false);
// Update the result with the completion
context.Variables.UpdateKeepingTrustState(completion);
context.Variables.Update(completion);
// Flag the result as untrusted if the prompt has been considered untrusted
if (!prompt.IsTrusted)
{
context.UntrustResult();
}
context.ModelResults = completionResults.Select(c => c.ModelResult).ToArray();
}
catch (AIException ex)
@@ -260,8 +228,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
// If the function is invoked manually, the user might have left out the skill collection
context.Skills ??= this._skillCollection;
var validateContextResult = await this.TrustServiceInstance.ValidateContextAsync(this, context).ConfigureAwait(false);
if (this.IsSemantic)
{
this.AddDefaultValues(context.Variables);
@@ -283,12 +249,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
}
}
// If the context has been considered untrusted, make sure the output of the function is also untrusted
if (!validateContextResult)
{
context.UntrustResult();
}
return context;
}
@@ -367,7 +327,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
private IReadOnlySkillCollection? _skillCollection;
private Lazy<ITextCompletion>? _aiService = null;
private CompleteRequestSettings _aiRequestSettings = new();
private readonly ITrustService _trustService;
private struct MethodDetails
{
@@ -375,7 +334,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
public List<ParameterView> Parameters { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsSensitive { get; set; }
}
private static async Task<string> GetCompletionsResultContentAsync(IReadOnlyList<ITextResult> completions, CancellationToken cancellationToken = default)
@@ -391,8 +349,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
string functionName,
string description,
bool isSemantic = false,
bool isSensitive = false,
ITrustService? trustService = null,
ILogger? log = null)
{
Verify.NotNull(delegateFunction);
@@ -402,14 +358,10 @@ public sealed class SKFunction : ISKFunction, IDisposable
this._log = log ?? NullLogger.Instance;
// If no trust service is specified, use the default implementation
this._trustService = trustService ?? TrustService.DefaultTrusted;
this._function = delegateFunction;
this.Parameters = parameters;
this.IsSemantic = isSemantic;
this.IsSensitive = isSensitive;
this.Name = functionName;
this.SkillName = skillName;
this.Description = description;
@@ -464,7 +416,6 @@ public sealed class SKFunction : ISKFunction, IDisposable
{
Name = functionName!,
Description = description ?? string.Empty,
IsSensitive = functionAttribute?.IsSensitive ?? false,
};
(result.Function, result.Parameters) = GetDelegateInfo(target, method);
@@ -612,7 +563,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
method.GetCustomAttribute<SKFunctionInputAttribute>(inherit: true) is SKFunctionInputAttribute inputAttr)
{
sawFirstParameter = true;
return (static (SKContext ctx) => ctx.Variables.Input.Value, inputAttr.ToParameterView());
return (static (SKContext ctx) => ctx.Variables.Input, inputAttr.ToParameterView());
}
// Use either the parameter's optional default value as contained in parameter metadata (e.g. `string s = "hello"`)
@@ -655,7 +606,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
Func<SKContext, object?> parameterFunc = (SKContext ctx) =>
{
// 1. Use the value of the variable if it exists.
if (ctx.Variables.Get(name, out string value))
if (ctx.Variables.TryGetValue(name, out string? value))
{
return Process(value);
}
@@ -669,7 +620,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
// 3. Otherwise, use "input" if this is the first (or only) parameter.
if (fallBackToInput)
{
return Process(ctx.Variables.Input.Value);
return Process(ctx.Variables.Input);
}
// 4. Otherwise, fail.
@@ -763,7 +714,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
{
return static (result, context) =>
{
context.Variables.UpdateKeepingTrustState((string?)result);
context.Variables.Update((string?)result);
return Task.FromResult(context);
};
}
@@ -772,7 +723,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
{
return async static (result, context) =>
{
context.Variables.UpdateKeepingTrustState(await ((Task<string>)ThrowIfNullResult(result)).ConfigureAwait(false));
context.Variables.Update(await ((Task<string>)ThrowIfNullResult(result)).ConfigureAwait(false));
return context;
};
}
@@ -781,7 +732,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
{
return async static (result, context) =>
{
context.Variables.UpdateKeepingTrustState(await ((ValueTask<string>)ThrowIfNullResult(result)).ConfigureAwait(false));
context.Variables.Update(await ((ValueTask<string>)ThrowIfNullResult(result)).ConfigureAwait(false));
return context;
};
}
@@ -797,7 +748,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
return (result, context) =>
{
context.Variables.UpdateKeepingTrustState(formatter(result, context.Culture));
context.Variables.Update(formatter(result, context.Culture));
return Task.FromResult(context);
};
}
@@ -813,7 +764,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
return async (result, context) =>
{
await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false);
context.Variables.UpdateKeepingTrustState(taskResultFormatter(taskResultGetter.Invoke(result!, Array.Empty<object>()), context.Culture));
context.Variables.Update(taskResultFormatter(taskResultGetter.Invoke(result!, Array.Empty<object>()), context.Culture));
return context;
};
}
@@ -922,7 +873,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
// If that fails, try with the invariant culture and allow any exception to propagate.
try
{
return converter.ConvertFromString(context: null, cultureInfo ?? CultureInfo.CurrentCulture, input);
return converter.ConvertFromString(context: null, cultureInfo, input);
}
catch (Exception e) when (!e.IsCriticalException() && cultureInfo != CultureInfo.InvariantCulture)
{
@@ -974,7 +925,7 @@ public sealed class SKFunction : ISKFunction, IDisposable
return null!;
}
return converter.ConvertToString(context: null, cultureInfo ?? CultureInfo.InvariantCulture, input);
return converter.ConvertToString(context: null, cultureInfo, input);
};
}
@@ -31,7 +31,7 @@ public static class SKFunctionExtensions
/// <param name="skFunction">Semantic function</param>
/// <param name="maxTokens">Tokens count</param>
/// <returns>Self instance</returns>
public static ISKFunction UseMaxTokens(this ISKFunction skFunction, int maxTokens)
public static ISKFunction UseMaxTokens(this ISKFunction skFunction, int? maxTokens)
{
skFunction.RequestSettings.MaxTokens = maxTokens;
return skFunction;
@@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Security;
using Microsoft.SemanticKernel.SkillDefinition;
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
@@ -124,7 +123,7 @@ internal sealed class CodeBlock : Block, ICodeRendering
this.Log.LogTrace("Passing variable/value: `{0}`", this._tokens[1].Content);
string input = ((ITextRendering)this._tokens[1]).Render(contextClone.Variables);
// Keep previous trust information when updating the input
contextClone.Variables.Update(new TrustAwareString(input, contextClone.Variables.IsAllTrusted()));
contextClone.Variables.Update(input);
}
try
@@ -138,19 +137,6 @@ internal sealed class CodeBlock : Block, ICodeRendering
contextClone.Fail(ex.Message, ex);
}
// If the output of the function is not trusted, tag the main context as untrusted as well.
// This is important for cases where there is a function call within a prompt template and
// the result of the function call is untrusted. In such cases, we need to propagate the
// untrusted tag back to the main context.
// TODO: we might want to evaluate updating the template engine to rely on TrustAwareStrings that already
// include trust information. This way, the rendered prompt will be a TrustAwareString that already
// carries trust information, so this won't be needed.
// (e.g. concatenating strings A (trusted) and B (untrusted), will result in a string that is untrusted)
if (!contextClone.IsTrusted)
{
context.UntrustAll();
}
if (contextClone.ErrorOccurred)
{
var errorMsg = $"Function `{fBlock.Content}` execution failed. {contextClone.LastException?.GetType().FullName}: {contextClone.LastErrorDescription}";
@@ -189,7 +189,6 @@ public static class KernelGrpcExtensions
description: operation.Name,
skillName: skillName,
functionName: operation.Name,
isSensitive: false,
log: kernel.Log);
return kernel.RegisterCustomFunction(function);
@@ -301,7 +301,6 @@ public static class KernelOpenApiExtensions
description: operation.Description,
skillName: skillName,
functionName: ConvertOperationIdToValidFunctionName(operation.Id, logger),
isSensitive: false,
log: logger);
return kernel.RegisterCustomFunction(function);
@@ -28,7 +28,7 @@ internal sealed class XunitLogger<T> : ILogger<T>, IDisposable
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc/>
public IDisposable BeginScope<TState>(TState state) where TState : notnull
public IDisposable BeginScope<TState>(TState state)
=> this;
/// <inheritdoc/>
+22 -22
View File
@@ -1237,13 +1237,13 @@ files = [
[[package]]
name = "ipykernel"
version = "6.23.3"
version = "6.24.0"
description = "IPython Kernel for Jupyter"
optional = false
python-versions = ">=3.8"
files = [
{file = "ipykernel-6.23.3-py3-none-any.whl", hash = "sha256:bc00662dc44d4975b668cdb5fefb725e38e9d8d6e28441a519d043f38994922d"},
{file = "ipykernel-6.23.3.tar.gz", hash = "sha256:dd4e18116357f36a1e459b3768412371bee764c51844cbf25c4ed1eb9cae4a54"},
{file = "ipykernel-6.24.0-py3-none-any.whl", hash = "sha256:2f5fffc7ad8f1fd5aadb4e171ba9129d9668dbafa374732cf9511ada52d6547f"},
{file = "ipykernel-6.24.0.tar.gz", hash = "sha256:29cea0a716b1176d002a61d0b0c851f34536495bc4ef7dd0222c88b41b816123"},
]
[package.dependencies]
@@ -2892,28 +2892,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "ruff"
version = "0.0.275"
version = "0.0.277"
description = "An extremely fast Python linter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.0.275-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:5e6554a072e7ce81eb6f0bec1cebd3dcb0e358652c0f4900d7d630d61691e914"},
{file = "ruff-0.0.275-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1cc599022fe5ffb143a965b8d659eb64161ab8ab4433d208777eab018a1aab67"},
{file = "ruff-0.0.275-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5206fc1cd8c1c1deadd2e6360c0dbcd690f1c845da588ca9d32e4a764a402c60"},
{file = "ruff-0.0.275-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c4e6468da26f77b90cae35319d310999f471a8c352998e9b39937a23750149e"},
{file = "ruff-0.0.275-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dbdea02942131dbc15dd45f431d152224f15e1dd1859fcd0c0487b658f60f1a"},
{file = "ruff-0.0.275-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:22efd9f41af27ef8fb9779462c46c35c89134d33e326c889971e10b2eaf50c63"},
{file = "ruff-0.0.275-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c09662112cfa22d7467a19252a546291fd0eae4f423e52b75a7a2000a1894db"},
{file = "ruff-0.0.275-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80043726662144876a381efaab88841c88e8df8baa69559f96b22d4fa216bef1"},
{file = "ruff-0.0.275-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5859ee543b01b7eb67835dfd505faa8bb7cc1550f0295c92c1401b45b42be399"},
{file = "ruff-0.0.275-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c8ace4d40a57b5ea3c16555f25a6b16bc5d8b2779ae1912ce2633543d4e9b1da"},
{file = "ruff-0.0.275-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8347fc16aa185aae275906c4ac5b770e00c896b6a0acd5ba521f158801911998"},
{file = "ruff-0.0.275-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ec43658c64bfda44fd84bbea9da8c7a3b34f65448192d1c4dd63e9f4e7abfdd4"},
{file = "ruff-0.0.275-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:508b13f7ca37274cceaba4fb3ea5da6ca192356323d92acf39462337c33ad14e"},
{file = "ruff-0.0.275-py3-none-win32.whl", hash = "sha256:6afb1c4422f24f361e877937e2a44b3f8176774a476f5e33845ebfe887dd5ec2"},
{file = "ruff-0.0.275-py3-none-win_amd64.whl", hash = "sha256:d9b264d78621bf7b698b6755d4913ab52c19bd28bee1a16001f954d64c1a1220"},
{file = "ruff-0.0.275-py3-none-win_arm64.whl", hash = "sha256:a19ce3bea71023eee5f0f089dde4a4272d088d5ac0b675867e074983238ccc65"},
{file = "ruff-0.0.275.tar.gz", hash = "sha256:a63a0b645da699ae5c758fce19188e901b3033ec54d862d93fcd042addf7f38d"},
{file = "ruff-0.0.277-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:3250b24333ef419b7a232080d9724ccc4d2da1dbbe4ce85c4caa2290d83200f8"},
{file = "ruff-0.0.277-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:3e60605e07482183ba1c1b7237eca827bd6cbd3535fe8a4ede28cbe2a323cb97"},
{file = "ruff-0.0.277-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7baa97c3d7186e5ed4d5d4f6834d759a27e56cf7d5874b98c507335f0ad5aadb"},
{file = "ruff-0.0.277-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74e4b206cb24f2e98a615f87dbe0bde18105217cbcc8eb785bb05a644855ba50"},
{file = "ruff-0.0.277-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:479864a3ccd8a6a20a37a6e7577bdc2406868ee80b1e65605478ad3b8eb2ba0b"},
{file = "ruff-0.0.277-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:468bfb0a7567443cec3d03cf408d6f562b52f30c3c29df19927f1e0e13a40cd7"},
{file = "ruff-0.0.277-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f32ec416c24542ca2f9cc8c8b65b84560530d338aaf247a4a78e74b99cd476b4"},
{file = "ruff-0.0.277-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14a7b2f00f149c5a295f188a643ac25226ff8a4d08f7a62b1d4b0a1dc9f9b85c"},
{file = "ruff-0.0.277-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9879f59f763cc5628aa01c31ad256a0f4dc61a29355c7315b83c2a5aac932b5"},
{file = "ruff-0.0.277-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f612e0a14b3d145d90eb6ead990064e22f6f27281d847237560b4e10bf2251f3"},
{file = "ruff-0.0.277-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:323b674c98078be9aaded5b8b51c0d9c424486566fb6ec18439b496ce79e5998"},
{file = "ruff-0.0.277-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3a43fbe026ca1a2a8c45aa0d600a0116bec4dfa6f8bf0c3b871ecda51ef2b5dd"},
{file = "ruff-0.0.277-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:734165ea8feb81b0d53e3bf523adc2413fdb76f1264cde99555161dd5a725522"},
{file = "ruff-0.0.277-py3-none-win32.whl", hash = "sha256:88d0f2afb2e0c26ac1120e7061ddda2a566196ec4007bd66d558f13b374b9efc"},
{file = "ruff-0.0.277-py3-none-win_amd64.whl", hash = "sha256:6fe81732f788894a00f6ade1fe69e996cc9e485b7c35b0f53fb00284397284b2"},
{file = "ruff-0.0.277-py3-none-win_arm64.whl", hash = "sha256:2d4444c60f2e705c14cd802b55cd2b561d25bf4311702c463a002392d3116b22"},
{file = "ruff-0.0.277.tar.gz", hash = "sha256:2dab13cdedbf3af6d4427c07f47143746b6b95d9e4a254ac369a0edb9280a0d2"},
]
[[package]]
@@ -4030,4 +4030,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "18d8ee6959d05dd3cda4d33d69aa0baf2f9de98ac792bb54a119c2b7d57444a0"
content-hash = "9ae4e4236289823b370e24a704d54c886e213a81e03a96bb6381db4e24442ebe"
+1 -1
View File
@@ -19,7 +19,7 @@ pre-commit = "3.3.3"
black = {version = "23.3.0", allow-prereleases = true}
ipykernel = "^6.21.1"
pytest = "7.4.0"
ruff = "0.0.275"
ruff = "0.0.277"
pytest-asyncio = "0.21.0"
[tool.poetry.group.hugging_face.dependencies]
@@ -56,7 +56,9 @@ class SemanticTextMemory(SemanticTextMemoryBase):
):
await self._storage.create_collection_async(collection_name=collection)
embedding = await self._embeddings_generator.generate_embeddings_async([text])
embedding = (
await self._embeddings_generator.generate_embeddings_async([text])
)[0]
data = MemoryRecord.local_record(
id=id,
text=text,
@@ -94,7 +96,9 @@ class SemanticTextMemory(SemanticTextMemoryBase):
):
await self._storage.create_collection_async(collection_name=collection)
embedding = await self._embeddings_generator.generate_embeddings_async([text])
embedding = (
await self._embeddings_generator.generate_embeddings_async([text])
)[0]
data = MemoryRecord.reference_record(
external_id=external_id,
source_name=external_source_name,
@@ -142,9 +146,9 @@ class SemanticTextMemory(SemanticTextMemoryBase):
Returns:
List[MemoryQueryResult] -- The list of MemoryQueryResult found.
"""
query_embedding = await self._embeddings_generator.generate_embeddings_async(
[query]
)
query_embedding = (
await self._embeddings_generator.generate_embeddings_async([query])
)[0]
results = await self._storage.get_nearest_matches_async(
collection_name=collection,
embedding=query_embedding,
+153 -70
View File
@@ -5,7 +5,8 @@ For plain text, split looking at new lines first, then periods, and so on.
For markdown, split looking at punctuation first, and so on.
"""
import os
from typing import List
import re
from typing import Callable, List, Tuple
NEWLINE = os.linesep
@@ -36,46 +37,95 @@ MD_SPLIT_OPTIONS = [
]
def split_plaintext_lines(text: str, max_token_per_line: int) -> List[str]:
def _token_counter(text: str) -> int:
"""
Count the number of tokens in a string.
TODO: chunking methods should be configurable to allow for different
tokenization strategies depending on the model to be called.
For now, we use an extremely rough estimate.
"""
return len(text) // 4
def split_plaintext_lines(
text: str, max_token_per_line: int, token_counter: Callable = _token_counter
) -> List[str]:
"""
Split plain text into lines.
it will split on new lines first, and then on punctuation.
"""
return _split_text_lines(text, max_token_per_line, True)
return _split_text_lines(
text=text,
max_token_per_line=max_token_per_line,
trim=True,
token_counter=token_counter,
)
def split_markdown_lines(text: str, max_token_per_line: int) -> List[str]:
def split_markdown_lines(
text: str, max_token_per_line: int, token_counter: Callable = _token_counter
) -> List[str]:
"""
Split markdown into lines.
It will split on punctuation first, and then on space and new lines.
"""
return _split_markdown_lines(text, max_token_per_line, True)
return _split_markdown_lines(
text=text,
max_token_per_line=max_token_per_line,
trim=True,
token_counter=token_counter,
)
def split_plaintext_paragraph(text: List[str], max_tokens: int) -> List[str]:
def split_plaintext_paragraph(
text: List[str], max_tokens: int, token_counter: Callable = _token_counter
) -> List[str]:
"""
Split plain text into paragraphs.
"""
split_lines = []
for line in text:
split_lines.extend(_split_text_lines(line, max_tokens, True))
split_lines.extend(
_split_text_lines(
text=line,
max_token_per_line=max_tokens,
trim=True,
token_counter=token_counter,
)
)
return _split_text_paragraph(split_lines, max_tokens)
return _split_text_paragraph(
text=split_lines, max_tokens=max_tokens, token_counter=token_counter
)
def split_markdown_paragraph(text: List[str], max_tokens: int) -> List[str]:
def split_markdown_paragraph(
text: List[str], max_tokens: int, token_counter: Callable = _token_counter
) -> List[str]:
"""
Split markdown into paragraphs.
"""
split_lines = []
for line in text:
split_lines.extend(_split_markdown_lines(line, max_tokens, False))
split_lines.extend(
_split_markdown_lines(
text=line,
max_token_per_line=max_tokens,
trim=False,
token_counter=token_counter,
)
)
return _split_text_paragraph(split_lines, max_tokens)
return _split_text_paragraph(
text=split_lines, max_tokens=max_tokens, token_counter=token_counter
)
def _split_text_paragraph(text: List[str], max_tokens: int) -> List[str]:
def _split_text_paragraph(
text: List[str], max_tokens: int, token_counter: Callable
) -> List[str]:
"""
Split text into paragraphs.
"""
@@ -86,8 +136,8 @@ def _split_text_paragraph(text: List[str], max_tokens: int) -> List[str]:
current_paragraph = []
for line in text:
num_tokens_line = _token_count(line)
num_tokens_paragraph = _token_count("".join(current_paragraph))
num_tokens_line = token_counter(line)
num_tokens_paragraph = token_counter("".join(current_paragraph))
if (
num_tokens_paragraph + num_tokens_line + 1 >= max_tokens
@@ -109,7 +159,7 @@ def _split_text_paragraph(text: List[str], max_tokens: int) -> List[str]:
last_para = paragraphs[-1]
sec_last_para = paragraphs[-2]
if _token_count(last_para) < max_tokens / 4:
if token_counter(last_para) < max_tokens / 4:
last_para_tokens = last_para.split(" ")
sec_last_para_tokens = sec_last_para.split(" ")
last_para_token_count = len(last_para_tokens)
@@ -125,27 +175,44 @@ def _split_text_paragraph(text: List[str], max_tokens: int) -> List[str]:
return paragraphs
def _split_markdown_lines(text: str, max_token_per_line: int, trim: bool) -> List[str]:
def _split_markdown_lines(
text: str, max_token_per_line: int, trim: bool, token_counter: Callable
) -> List[str]:
"""
Split markdown into lines.
"""
lines = _split_str_lines(text, max_token_per_line, MD_SPLIT_OPTIONS, trim)
return lines
return _split_str_lines(
text=text,
max_tokens=max_token_per_line,
separators=MD_SPLIT_OPTIONS,
trim=trim,
token_counter=token_counter,
)
def _split_text_lines(text: str, max_token_per_line: int, trim: bool) -> List[str]:
def _split_text_lines(
text: str, max_token_per_line: int, trim: bool, token_counter: Callable
) -> List[str]:
"""
Split text into lines.
"""
lines = _split_str_lines(text, max_token_per_line, TEXT_SPLIT_OPTIONS, trim)
return lines
return _split_str_lines(
text=text,
max_tokens=max_token_per_line,
separators=TEXT_SPLIT_OPTIONS,
trim=trim,
token_counter=token_counter,
)
def _split_str_lines(
text: str, max_tokens: int, separators: List[List[str]], trim: bool
text: str,
max_tokens: int,
separators: List[List[str]],
trim: bool,
token_counter: Callable,
) -> List[str]:
if not text:
return []
@@ -155,65 +222,82 @@ def _split_str_lines(
was_split = False
for split_option in separators:
if not lines:
lines, was_split = _split_str(text, max_tokens, split_option, trim)
lines, was_split = _split_str(
text=text,
max_tokens=max_tokens,
separators=split_option,
trim=trim,
token_counter=token_counter,
)
else:
lines, was_split = _split_list(lines, max_tokens, split_option, trim)
if not was_split:
lines, was_split = _split_list(
text=lines,
max_tokens=max_tokens,
separators=split_option,
trim=trim,
token_counter=token_counter,
)
if was_split:
break
return lines
def _split_str(
text: str, max_tokens: int, separators: List[str], trim: bool
) -> List[str]:
text: str,
max_tokens: int,
separators: List[str],
trim: bool,
token_counter: Callable,
) -> Tuple[List[str], bool]:
"""
Split text into lines.
"""
if not text:
return []
input_was_split = False
text = text.strip() if trim else text
if not text:
return [], input_was_split
if trim:
text = text.strip()
text_as_is = [text]
if _token_count(text) <= max_tokens:
if token_counter(text) <= max_tokens:
return text_as_is, input_was_split
input_was_split = True
half = int(len(text) / 2)
half = len(text) // 2
cutpoint = -1
if not separators:
cutpoint = half
elif set(separators) & set(text) and len(text) > 2:
for index, text_char in enumerate(text):
if text_char not in separators:
continue
if abs(half - index) < abs(half - cutpoint):
cutpoint = index + 1
regex_separators = re.compile("|".join(re.escape(s) for s in separators))
min_dist = half
for match in re.finditer(regex_separators, text):
end = match.end()
dist = abs(half - end)
if dist < min_dist:
min_dist = dist
cutpoint = end
elif end > half:
# distance is increasing, so we can stop searching
break
else:
return text_as_is, input_was_split
if 0 < cutpoint < len(text):
lines = []
first_split, has_split1 = _split_str(
text[:cutpoint], max_tokens, separators, trim
)
second_split, has_split2 = _split_str(
text[cutpoint:], max_tokens, separators, trim
)
lines.extend(first_split)
lines.extend(second_split)
input_was_split = has_split1 or has_split2
for text_part in [text[:cutpoint], text[cutpoint:]]:
split, has_split = _split_str(
text=text_part,
max_tokens=max_tokens,
separators=separators,
trim=trim,
token_counter=token_counter,
)
lines.extend(split)
input_was_split = input_was_split or has_split
else:
return text_as_is, input_was_split
@@ -221,30 +305,29 @@ def _split_str(
def _split_list(
text: List[str], max_tokens: int, separators: List[str], trim: bool
) -> List[str]:
text: List[str],
max_tokens: int,
separators: List[str],
trim: bool,
token_counter: Callable,
) -> Tuple[List[str], bool]:
"""
Split list of string into lines.
"""
if not text:
return []
return [], False
lines = []
input_was_split = False
for line in text:
split_str, was_split = _split_str(line, max_tokens, separators, trim)
split_str, was_split = _split_str(
text=line,
max_tokens=max_tokens,
separators=separators,
trim=trim,
token_counter=token_counter,
)
lines.extend(split_str)
input_was_split = input_was_split or was_split
return lines, input_was_split
def _token_count(text: str) -> int:
"""
Count the number of tokens in a string.
TODO: chunking methods should be configurable to allow for different
tokenization strategies depending on the model to be called.
For now, we use an extremely rough estimate.
"""
return int(len(text) / 4)
@@ -25,12 +25,12 @@ pytestmark = pytest.mark.skipif(
def get_pinecone_config():
if "Python_Integration_Tests" in os.environ:
api_key = os.environ["Pinecone__ApiKey"]
org_id = None
environment = os.environ["Pinecone__Environment"]
else:
# Load credentials from .env file
api_key, org_id = sk.pinecone_settings_from_dot_env()
api_key, environment = sk.pinecone_settings_from_dot_env()
return api_key, org_id
return api_key, environment
@pytest.fixture
@@ -11,6 +11,44 @@ from semantic_kernel.text import (
NEWLINE = os.linesep
def test_split_plain_text_lines_with_token_count():
"""Test split_plain_text_lines() with external token counter"""
text = "This is a test of the emergency broadcast system. This is only a test."
max_token_per_line = 8
expected = [
"This is a test of the",
"emergency",
"broadcast system.",
"This is only a test.",
]
split = split_plaintext_lines(
text=text,
max_token_per_line=max_token_per_line,
token_counter=lambda x: len(x) // 3,
)
assert expected == split
def test_split_plain_text_lines_half():
"""Test split_plain_text_lines() with external token counter"""
text_1 = "This is a test of. cutting. at the half point."
text_2 = "This is a test of . cutting. at the half point."
max_token_per_line = 10
expected_1 = ["This is a test of. cutting.", "at the half point."]
split_1 = split_plaintext_lines(text=text_1, max_token_per_line=max_token_per_line)
assert expected_1 == split_1
expected_2 = ["This is a test of .", "cutting. at the half point."]
split_2 = split_plaintext_lines(text=text_2, max_token_per_line=max_token_per_line)
assert expected_2 == split_2
def test_split_plain_text_lines():
"""Test split_plain_text_lines()"""
+3 -3
View File
@@ -9311,9 +9311,9 @@ [email protected]:
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tough-cookie@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==
version "4.1.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf"
integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==
dependencies:
psl "^1.1.33"
punycode "^2.1.1"
@@ -9287,9 +9287,9 @@ [email protected]:
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tough-cookie@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==
version "4.1.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf"
integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==
dependencies:
psl "^1.1.33"
punycode "^2.1.1"
@@ -9287,9 +9287,9 @@ [email protected]:
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tough-cookie@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==
version "4.1.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf"
integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==
dependencies:
psl "^1.1.33"
punycode "^2.1.1"
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.6.33706.43
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CopilotChatWebApi", "webapi\CopilotChatWebApi.csproj", "{5252E68F-B653-44CE-9A32-360A75C54E0E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5252E68F-B653-44CE-9A32-360A75C54E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5252E68F-B653-44CE-9A32-360A75C54E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5252E68F-B653-44CE-9A32-360A75C54E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5252E68F-B653-44CE-9A32-360A75C54E0E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {971570D3-60EA-4EE4-980C-0BDA3E66E741}
EndGlobalSection
EndGlobal
@@ -76,7 +76,7 @@ chmod +x ./deploy-webapi.sh
### App registration (identity)
You will need an Azure Active Directory (AAD) application registration.
> For details on creating an application registration, go [here](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
- Select `Single-page application (SPA)` as platform type, and set the Web redirect URI to `http://localhost:3000`
- Select `Single-page application (SPA)` as platform type, and set the redirect URI to `http://localhost:3000`
- Select `Accounts in any organizational directory and personal Microsoft Accounts` as supported account types for this sample.
- Make a note of the `Application (client) ID` from the Azure Portal for use in the `Deploy` below.
@@ -98,7 +98,7 @@ npm install -g @azure/static-web-apps-cli
./deploy-webapp.sh --subscription {YOUR_SUBSCRIPTION_ID} --resource-group rg-{YOUR_DEPLOYMENT_NAME} --deployment-name {YOUR_DEPLOYMENT_NAME} --application-id {YOUR_APPLICATION_ID}
```
Your CoplitChat application is now deployed!
Your CopilotChat application is now deployed!
# Appendix
@@ -35,16 +35,16 @@ if ($LASTEXITCODE -ne 0) {
Write-Host "Setting subscription to '$Subscription'..."
az account set -s $Subscription
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
exit $LASTEXITCODE
}
Write-Host "Getting deployment outputs..."
$deployment=$(az deployment group show --name $DeploymentName --resource-group $ResourceGroupName --output json | ConvertFrom-Json)
$webappUrl=$deployment.properties.outputs.webappUrl.value
$webappName=$deployment.properties.outputs.webappName.value
$webapiUrl=$deployment.properties.outputs.webapiUrl.value
$webapiName=$deployment.properties.outputs.webapiName.value
$webapiApiKey=($(az webapp config appsettings list --name $webapiName --resource-group $ResourceGroupName | ConvertFrom-JSON) | Where-Object -Property name -EQ -Value Authorization:ApiKey).value
$deployment = $(az deployment group show --name $DeploymentName --resource-group $ResourceGroupName --output json | ConvertFrom-Json)
$webappUrl = $deployment.properties.outputs.webappUrl.value
$webappName = $deployment.properties.outputs.webappName.value
$webapiUrl = $deployment.properties.outputs.webapiUrl.value
$webapiName = $deployment.properties.outputs.webapiName.value
$webapiApiKey = ($(az webapp config appsettings list --name $webapiName --resource-group $ResourceGroupName | ConvertFrom-JSON) | Where-Object -Property name -EQ -Value Authorization:ApiKey).value
Write-Host "webappUrl: $webappUrl"
Write-Host "webappName: $webappName"
Write-Host "webapiName: $webapiName"
@@ -53,7 +53,7 @@ Write-Host "webapiUrl: $webapiUrl"
# Set UTF8 as default encoding for Out-File
$PSDefaultParameterValues['Out-File:Encoding'] = 'ascii'
$envFilePath="$PSScriptRoot/../webapp/.env"
$envFilePath = "$PSScriptRoot/../webapp/.env"
Write-Host "Writing environment variables to '$envFilePath'..."
"REACT_APP_BACKEND_URI=https://$webapiUrl/" | Out-File -FilePath $envFilePath
"REACT_APP_AAD_AUTHORITY=https://login.microsoftonline.com/common" | Out-File -FilePath $envFilePath -Append
@@ -74,7 +74,7 @@ Push-Location -Path "$PSScriptRoot/../webapp"
Write-Host "Installing yarn dependencies..."
yarn install
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
exit $LASTEXITCODE
}
Write-Host "Building webapp..."
@@ -89,19 +89,34 @@ if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
Write-Host "Updating AAD App registration..."
az ad app update --id $ApplicationClientId --web-redirect-uris "https://$webappUrl"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$origin = "https://$webappUrl"
Write-Host "Ensuring CORS origin '$origin' to webapi '$webapiName'..."
if (-not ((az webapp cors show --name $webapiName --resource-group $ResourceGroupName --subscription $Subscription | ConvertFrom-Json).allowedOrigins -contains $origin)) {
az webapp cors add --name $webapiName --resource-group $ResourceGroupName --subscription $Subscription --allowed-origins $origin
}
Write-Host "Ensuring '$origin' is included in AAD app registration's redirect URIs..."
$objectId = (az ad app show --id $ApplicationClientId | ConvertFrom-Json).id
$redirectUris = (az rest --method GET --uri "https://graph.microsoft.com/v1.0/applications/$objectId" --headers 'Content-Type=application/json' | ConvertFrom-Json).spa.redirectUris
if ($redirectUris -notcontains "$origin") {
$redirectUris += "$origin"
$body = "{spa:{redirectUris:["
foreach ($uri in $redirectUris) {
$body += "'$uri',"
}
$body += "]}}"
az rest `
--method PATCH `
--uri "https://graph.microsoft.com/v1.0/applications/$objectId" `
--headers 'Content-Type=application/json' `
--body $body
}
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
Pop-Location
Write-Host "To verify your deployment, go to 'https://$webappUrl' in your browser."
@@ -90,9 +90,6 @@ cat $SWA_CONFIG_FILE_PATH
pushd "$SCRIPT_ROOT/../webapp"
ORIGIN="https://$WEB_APP_URL"
echo "Ensuring CORS origin '$ORIGIN' to webapi '$WEB_API_NAME'..."
echo "Installing yarn dependencies..."
yarn install
if [ $? -ne 0 ]; then
@@ -114,21 +111,32 @@ if [ $? -ne 0 ]; then
exit 1
fi
echo "Updating AAD App registration..."
az ad app update --id $ApplicationClientId --web-redirect-uris "https://$webappUrl"
if [ $? -ne 0 ]; then
echo "update the AAD App registration failed"
exit 1
fi
ORIGIN="https://$WEB_APP_URL"
echo "Ensuring CORS origin '$ORIGIN' to webapi '$WEB_API_NAME'..."
CORS_RESULT=$(az webapp cors show --name $WEB_API_NAME --resource-group $RESOURCE_GROUP --subscription $SUBSCRIPTION | jq '.allowedOrigins | index("$ORIGIN")')
if [[ "$CORS_RESULT" == "null" ]]; then
echo "Adding CORS origin '$ORIGIN' to webapi '$WEB_API_NAME'..."
az webapp cors add --name $WEB_API_NAME --resource-group $RESOURCE_GROUP --subscription $SUBSCRIPTION --allowed-origins $origin
az webapp cors add --name $WEB_API_NAME --resource-group $RESOURCE_GROUP --subscription $SUBSCRIPTION --allowed-origins $ORIGIN
fi
echo "Ensuring '$ORIGIN' is included in AAD app registration's redirect URIs..."
eval OBJECT_ID=$(az ad app show --id $APPLICATION_ID | jq -r '.id')
REDIRECT_URIS=$(az rest --method GET --uri "https://graph.microsoft.com/v1.0/applications/$OBJECT_ID" --headers 'Content-Type=application/json' | jq -r '.spa.redirectUris')
if [[ ! "$REDIRECT_URIS" =~ "$ORIGIN" ]]; then
BODY="{spa:{redirectUris:['"
eval BODY+=$(echo $REDIRECT_URIS | jq $'join("\',\'")')
BODY+="','$ORIGIN']}}"
az rest \
--method PATCH \
--uri "https://graph.microsoft.com/v1.0/applications/$OBJECT_ID" \
--headers 'Content-Type=application/json' \
--body $BODY
fi
if [ $? -ne 0 ]; then
echo "Failed to update app registration"
exit 1
fi
popd
@@ -37,7 +37,7 @@ internal static class ConfigExtensions
reloadOnChange: true);
// For settings from Key Vault, see https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-8.0
string? keyVaultUri = builderContext.Configuration["KeyVaultUri"];
string? keyVaultUri = builderContext.Configuration["Service:KeyVault"];
if (!string.IsNullOrWhiteSpace(keyVaultUri))
{
configBuilder.AddAzureKeyVault(
@@ -22,6 +22,7 @@ using Microsoft.SemanticKernel.Skills.MsGraph;
using Microsoft.SemanticKernel.Skills.MsGraph.Connectors;
using Microsoft.SemanticKernel.Skills.MsGraph.Connectors.Client;
using Microsoft.SemanticKernel.Skills.OpenAPI.Authentication;
using Microsoft.SemanticKernel.Skills.OpenAPI.Extensions;
using SemanticKernel.Service.CopilotChat.Hubs;
using SemanticKernel.Service.CopilotChat.Models;
using SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
@@ -162,7 +163,10 @@ public class ChatController : ControllerBase, IDisposable
using HttpClient importHttpClient = new(retryHandler, false);
importHttpClient.DefaultRequestHeaders.Add("User-Agent", "Microsoft.CopilotChat");
await planner.Kernel.ImportChatGptPluginSkillFromUrlAsync("KlarnaShoppingSkill", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"),
importHttpClient);
new OpenApiSkillExecutionParameters
{
HttpClient = importHttpClient,
});
}
// GitHub
@@ -173,7 +177,10 @@ public class ChatController : ControllerBase, IDisposable
await planner.Kernel.ImportOpenApiSkillFromFileAsync(
skillName: "GitHubSkill",
filePath: Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CopilotChat", "Skills", "OpenApiSkills/GitHubSkill/openapi.json"),
authCallback: authenticationProvider.AuthenticateRequestAsync);
new OpenApiSkillExecutionParameters
{
AuthCallback = authenticationProvider.AuthenticateRequestAsync,
});
}
// Jira
@@ -186,8 +193,11 @@ public class ChatController : ControllerBase, IDisposable
await planner.Kernel.ImportOpenApiSkillFromFileAsync(
skillName: "JiraSkill",
filePath: Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CopilotChat", "Skills", "OpenApiSkills/JiraSkill/openapi.json"),
authCallback: authenticationProvider.AuthenticateRequestAsync,
serverUrlOverride: hasServerUrlOverride ? new Uri(serverUrlOverride!) : null);
new OpenApiSkillExecutionParameters
{
AuthCallback = authenticationProvider.AuthenticateRequestAsync,
ServerUrlOverride = hasServerUrlOverride ? new Uri(serverUrlOverride!) : null,
});
}
// Microsoft Graph
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text.Json;
@@ -93,10 +94,9 @@ public class ChatSkill
/// Extract user intent from the conversation history.
/// </summary>
/// <param name="context">The SKContext.</param>
[SKFunction("Extract user intent")]
[SKFunctionName("ExtractUserIntent")]
[SKFunctionContextParameter(Name = "chatId", Description = "Chat ID to extract history from")]
[SKFunctionContextParameter(Name = "audience", Description = "The audience the chat bot is interacting with.")]
[SKFunction, Description("Extract user intent")]
[SKParameter("chatId", "Chat ID to extract history from")]
[SKParameter("audience", "The audience the chat bot is interacting with.")]
public async Task<string> ExtractUserIntentAsync(SKContext context)
{
var tokenLimit = this._promptOptions.CompletionTokenLimit;
@@ -140,9 +140,8 @@ public class ChatSkill
/// Extract the list of participants from the conversation history.
/// Note that only those who have spoken will be included.
/// </summary>
[SKFunction("Extract audience list")]
[SKFunctionName("ExtractAudience")]
[SKFunctionContextParameter(Name = "chatId", Description = "Chat ID to extract history from")]
[SKFunction, Description("Extract audience list")]
[SKParameter("chatId", "Chat ID to extract history from")]
public async Task<string> ExtractAudienceAsync(SKContext context)
{
var tokenLimit = this._promptOptions.CompletionTokenLimit;
@@ -184,15 +183,11 @@ public class ChatSkill
/// Extract chat history.
/// </summary>
/// <param name="context">Contains the 'tokenLimit' controlling the length of the prompt.</param>
[SKFunction("Extract chat history")]
[SKFunctionName("ExtractChatHistory")]
[SKFunctionContextParameter(Name = "chatId", Description = "Chat ID to extract history from")]
[SKFunctionContextParameter(Name = "tokenLimit", Description = "Maximum number of tokens")]
public async Task<string> ExtractChatHistoryAsync(SKContext context)
[SKFunction, Description("Extract chat history")]
public async Task<string> ExtractChatHistoryAsync(
[Description("Chat ID to extract history from")] string chatId,
[Description("Maximum number of tokens")] int tokenLimit)
{
var chatId = context["chatId"];
var tokenLimit = int.Parse(context["tokenLimit"], new NumberFormatInfo());
var messages = await this._chatMessageRepository.FindByChatIdAsync(chatId);
var sortedMessages = messages.OrderByDescending(m => m.Timestamp);
@@ -242,36 +237,19 @@ public class ChatSkill
/// messages to memory, and fill in the necessary context variables for completing the
/// prompt that will be rendered by the template engine.
/// </summary>
/// <param name="message"></param>
/// <param name="context">Contains the 'tokenLimit' and the 'contextTokenLimit' controlling the length of the prompt.</param>
[SKFunction("Get chat response")]
[SKFunctionName("Chat")]
[SKFunctionInput(Description = "The new message")]
[SKFunctionContextParameter(Name = "userId", Description = "Unique and persistent identifier for the user")]
[SKFunctionContextParameter(Name = "userName", Description = "Name of the user")]
[SKFunctionContextParameter(Name = "chatId", Description = "Unique and persistent identifier for the chat")]
[SKFunctionContextParameter(Name = "proposedPlan", Description = "Previously proposed plan that is approved")]
[SKFunctionContextParameter(Name = "messageType", Description = "Type of the message")]
[SKFunctionContextParameter(Name = "responseMessageId", Description = "ID of the response message for planner")]
public async Task<SKContext> ChatAsync(string message, SKContext context)
[SKFunction, Description("Get chat response")]
public async Task<SKContext> ChatAsync(
[Description("The new message")] string message,
[Description("Unique and persistent identifier for the user")] string userId,
[Description("Name of the user")] string userName,
[Description("Unique and persistent identifier for the chat")] string chatId,
[Description("Type of the message")] string messageType,
[Description("Previously proposed plan that is approved"), DefaultValue(null), SKName("proposedPlan")] string? planJson,
[Description("ID of the response message for planner"), DefaultValue(null), SKName("responseMessageId")] string? messageId,
SKContext context)
{
// TODO: check if user has access to the chat
var userId = context["userId"];
var userName = context["userName"];
var chatId = context["chatId"];
var messageType = context["messageType"];
// Save this new message to memory such that subsequent chat responses can use it
try
{
await this.SaveNewMessageAsync(message, userId, userName, chatId, messageType);
}
catch (Exception ex) when (!ex.IsCriticalException())
{
context.Log.LogError("Unable to save new message: {0}", ex.Message);
context.Fail($"Unable to save new message: {ex.Message}", ex);
return context;
}
await this.SaveNewMessageAsync(message, userId, userName, chatId, messageType);
// Clone the context to avoid modifying the original context variables.
var chatContext = Utilities.CopyContextWithVariablesClone(context);
@@ -280,16 +258,15 @@ public class ChatSkill
// Check if plan exists in ask's context variables.
// If plan was returned at this point, that means it was approved or cancelled.
// Update the response previously saved in chat history with state
if (context.Variables.TryGetValue("proposedPlan", out string? planJson)
&& !string.IsNullOrWhiteSpace(planJson)
&& context.Variables.TryGetValue("responseMessageId", out string? messageId))
if (!string.IsNullOrWhiteSpace(planJson) &&
!string.IsNullOrEmpty(messageId))
{
await this.UpdateResponseAsync(planJson, messageId);
}
var response = chatContext.Variables.ContainsKey("userCancelledPlan")
? "I am sorry the plan did not meet your goals."
: await this.GetChatResponseAsync(chatContext);
: await this.GetChatResponseAsync(chatId, chatContext);
if (chatContext.ErrorOccurred)
{
@@ -299,24 +276,14 @@ public class ChatSkill
// Retrieve the prompt used to generate the response
// and return it to the caller via the context variables.
var prompt = chatContext.Variables.ContainsKey("prompt")
? chatContext.Variables["prompt"]
: string.Empty;
chatContext.Variables.TryGetValue("prompt", out string? prompt);
prompt ??= string.Empty;
context.Variables.Set("prompt", prompt);
// Save this response to memory such that subsequent chat responses can use it
try
{
ChatMessage botMessage = await this.SaveNewResponseAsync(response, prompt, chatId);
context.Variables.Set("messageId", botMessage.Id);
context.Variables.Set("messageType", ((int)botMessage.Type).ToString(CultureInfo.InvariantCulture));
}
catch (Exception ex) when (!ex.IsCriticalException())
{
context.Log.LogError("Unable to save new response: {0}", ex.Message);
context.Fail($"Unable to save new response: {ex.Message}");
return context;
}
ChatMessage botMessage = await this.SaveNewResponseAsync(response, prompt, chatId);
context.Variables.Set("messageId", botMessage.Id);
context.Variables.Set("messageType", ((int)botMessage.Type).ToString(CultureInfo.InvariantCulture));
// Extract semantic chat memory
await SemanticChatMemoryExtractor.ExtractSemanticChatMemoryAsync(
@@ -336,7 +303,7 @@ public class ChatSkill
/// </summary>
/// <param name="chatContext">The SKContext.</param>
/// <returns>A response from the model.</returns>
private async Task<string> GetChatResponseAsync(SKContext chatContext)
private async Task<string> GetChatResponseAsync(string chatId, SKContext chatContext)
{
// 0. Get the audience
var audience = await this.GetAudienceAsync(chatContext);
@@ -371,7 +338,7 @@ public class ChatSkill
// 4. Query relevant semantic memories
var chatMemoriesTokenLimit = (int)(remainingToken * this._promptOptions.MemoriesResponseContextWeight);
var chatMemories = await this.QueryChatMemoriesAsync(chatContext, userIntent, chatMemoriesTokenLimit);
var chatMemories = await this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, chatId, chatMemoriesTokenLimit, chatContext.Memory);
if (chatContext.ErrorOccurred)
{
return string.Empty;
@@ -379,7 +346,7 @@ public class ChatSkill
// 5. Query relevant document memories
var documentContextTokenLimit = (int)(remainingToken * this._promptOptions.DocumentContextWeight);
var documentMemories = await this.QueryDocumentsAsync(chatContext, userIntent, documentContextTokenLimit);
var documentMemories = await this._documentMemorySkill.QueryDocumentsAsync(userIntent, chatId, documentContextTokenLimit, chatContext.Memory);
if (chatContext.ErrorOccurred)
{
return string.Empty;
@@ -391,7 +358,7 @@ public class ChatSkill
var chatContextTextTokenCount = remainingToken - Utilities.TokenCount(chatContextText);
if (chatContextTextTokenCount > 0)
{
var chatHistory = await this.GetChatHistoryAsync(chatContext, chatContextTextTokenCount);
var chatHistory = await this.ExtractChatHistoryAsync(chatId, chatContextTextTokenCount);
if (chatContext.ErrorOccurred)
{
return string.Empty;
@@ -490,62 +457,13 @@ public class ChatSkill
return userIntent;
}
/// <summary>
/// Helper function create the correct context variables to
/// extract chat history messages from the conversation history.
/// </summary>
private Task<string> GetChatHistoryAsync(SKContext context, int tokenLimit)
{
var contextVariables = new ContextVariables();
contextVariables.Set("chatId", context["chatId"]);
contextVariables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo()));
var chatHistoryContext = new SKContext(
contextVariables,
context.Memory,
context.Skills,
context.Log,
context.CancellationToken
);
var chatHistory = this.ExtractChatHistoryAsync(chatHistoryContext);
// Propagate the error
if (chatHistoryContext.ErrorOccurred)
{
context.Fail(chatHistoryContext.LastErrorDescription);
}
return chatHistory;
}
/// <summary>
/// Helper function create the correct context variables to
/// query chat memories from the chat memory store.
/// </summary>
private Task<string> QueryChatMemoriesAsync(SKContext context, string userIntent, int tokenLimit)
{
var contextVariables = new ContextVariables();
contextVariables.Set("chatId", context["chatId"]);
contextVariables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo()));
var chatMemoriesContext = new SKContext(
contextVariables,
context.Memory,
context.Skills,
context.Log,
context.CancellationToken
);
var chatMemories = this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, chatMemoriesContext);
// Propagate the error
if (chatMemoriesContext.ErrorOccurred)
{
context.Fail(chatMemoriesContext.LastErrorDescription);
}
return chatMemories;
return this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, context["chatId"], tokenLimit, context.Memory);
}
/// <summary>
@@ -554,27 +472,7 @@ public class ChatSkill
/// </summary>
private Task<string> QueryDocumentsAsync(SKContext context, string userIntent, int tokenLimit)
{
var contextVariables = new ContextVariables();
contextVariables.Set("chatId", context["chatId"]);
contextVariables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo()));
var documentMemoriesContext = new SKContext(
contextVariables,
context.Memory,
context.Skills,
context.Log,
context.CancellationToken
);
var documentMemories = this._documentMemorySkill.QueryDocumentsAsync(userIntent, documentMemoriesContext);
// Propagate the error
if (documentMemoriesContext.ErrorOccurred)
{
context.Fail(documentMemoriesContext.LastErrorDescription);
}
return documentMemories;
return this._documentMemorySkill.QueryDocumentsAsync(userIntent, context["chatId"], tokenLimit, context.Memory);
}
/// <summary>
@@ -1,12 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Globalization;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
using SemanticKernel.Service.CopilotChat.Options;
@@ -43,15 +42,13 @@ public class DocumentMemorySkill
/// </summary>
/// <param name="query">Query to match.</param>
/// <param name="context">The SkContext.</param>
[SKFunction("Query documents in the memory given a user message")]
[SKFunctionName("QueryDocuments")]
[SKFunctionInput(Description = "Query to match.")]
[SKFunctionContextParameter(Name = "chatId", Description = "ID of the chat that owns the documents")]
[SKFunctionContextParameter(Name = "tokenLimit", Description = "Maximum number of tokens")]
public async Task<string> QueryDocumentsAsync(string query, SKContext context)
[SKFunction, Description("Query documents in the memory given a user message")]
public async Task<string> QueryDocumentsAsync(
[Description("Query to match.")] string query,
[Description("ID of the chat that owns the documents")] string chatId,
[Description("Maximum number of tokens")] int tokenLimit,
ISemanticTextMemory textMemory)
{
string chatId = context.Variables["chatId"];
int tokenLimit = int.Parse(context.Variables["tokenLimit"], new NumberFormatInfo());
var remainingToken = tokenLimit;
// Search for relevant document snippets.
@@ -64,7 +61,7 @@ public class DocumentMemorySkill
List<MemoryQueryResult> relevantMemories = new();
foreach (var documentCollection in documentCollections)
{
var results = context.Memory.SearchAsync(
var results = textMemory.SearchAsync(
documentCollection,
query,
limit: 100,
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text.Json;
@@ -64,12 +65,12 @@ public class ExternalInformationSkill
/// <summary>
/// Extract relevant additional knowledge using a planner.
/// </summary>
[SKFunction("Acquire external information")]
[SKFunctionName("AcquireExternalInformation")]
[SKFunctionInput(Description = "The intent to whether external information is needed")]
[SKFunctionContextParameter(Name = "tokenLimit", Description = "Maximum number of tokens")]
[SKFunctionContextParameter(Name = "proposedPlan", Description = "Previously proposed plan that is approved")]
public async Task<string> AcquireExternalInformationAsync(string userIntent, SKContext context)
[SKFunction, Description("Acquire external information")]
[SKParameter("tokenLimit", "Maximum number of tokens")]
[SKParameter("proposedPlan", "Previously proposed plan that is approved")]
public async Task<string> AcquireExternalInformationAsync(
[Description("The intent to whether external information is needed")] string userIntent,
SKContext context)
{
FunctionsView functions = this._planner.Kernel.Skills.GetFunctionsView(true, true);
if (functions.NativeFunctions.IsEmpty && functions.SemanticFunctions.IsEmpty)
@@ -125,13 +126,13 @@ public class ExternalInformationSkill
if (plan.Steps.Count > 0)
{
// Parameters stored in plan's top level state
this.MergeContextIntoPlan(context.Variables, plan.State);
// Parameters stored in plan's top level
this.MergeContextIntoPlan(context.Variables, plan.Parameters);
// TODO: Improve Kernel to give developers option to skip this override
// (i.e., keep functions regardless of whether they're available in the planner's context or not)
Plan sanitizedPlan = this.SanitizePlan(plan, context);
sanitizedPlan.State.Update(plan.State);
sanitizedPlan.Parameters.Update(plan.Parameters);
this.ProposedPlan = new ProposedPlan(sanitizedPlan, this._planner.PlannerOptions!.Type, PlanState.NoOp);
}
@@ -1,12 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Globalization;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
using SemanticKernel.Service.CopilotChat.Options;
@@ -37,22 +36,20 @@ public class SemanticChatMemorySkill
/// <param name="query">Query to match.</param>
/// <param name="context">The SKContext</param>
/// <returns>A string containing the relevant memories.</returns>
[SKFunction("Query chat memories")]
[SKFunctionName("QueryMemories")]
[SKFunctionInput(Description = "Query to match.")]
[SKFunctionContextParameter(Name = "chatId", Description = "Chat ID to query history from")]
[SKFunctionContextParameter(Name = "tokenLimit", Description = "Maximum number of tokens")]
public async Task<string> QueryMemoriesAsync(string query, SKContext context)
[SKFunction, Description("Query chat memories")]
public async Task<string> QueryMemoriesAsync(
[Description("Query to match.")] string query,
[Description("Chat ID to query history from")] string chatId,
[Description("Maximum number of tokens")] int tokenLimit,
ISemanticTextMemory textMemory)
{
var chatId = context["chatId"];
var tokenLimit = int.Parse(context["tokenLimit"], new NumberFormatInfo());
var remainingToken = tokenLimit;
// Search for relevant memories.
List<MemoryQueryResult> relevantMemories = new();
foreach (var memoryName in this._promptOptions.MemoryMap.Keys)
{
var results = context.Memory.SearchAsync(
var results = textMemory.SearchAsync(
SemanticChatMemoryExtractor.MemoryCollectionName(chatId, memoryName),
query,
limit: 100,
@@ -11,13 +11,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.34.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.16.230615.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AI.OpenAI" Version="0.16.230615.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch" Version="0.16.230615.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="0.16.230615.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Skills.MsGraph" Version="0.16.230615.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Skills.OpenAPI" Version="0.16.230615.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Skills.Web" Version="0.16.230615.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.17.230629.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AI.OpenAI" Version="0.17.230629.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch" Version="0.17.230629.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="0.17.230629.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Skills.MsGraph" Version="0.17.230629.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Skills.OpenAPI" Version="0.17.230629.1-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Skills.Web" Version="0.17.230629.1-preview" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="2.11.1" />
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ComponentModel.DataAnnotations;
namespace SemanticKernel.Service.Options;
@@ -16,7 +15,7 @@ public class ServiceOptions
/// Configuration Key Vault URI
/// </summary>
[Url]
public Uri? KeyVaultUri { get; set; }
public string? KeyVault { get; set; }
/// <summary>
/// Local directory in which to load semantic skills.
@@ -12,8 +12,8 @@ using Microsoft.SemanticKernel.AI.Embeddings;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch;
using Microsoft.SemanticKernel.Connectors.Memory.Qdrant;
using Microsoft.SemanticKernel.CoreSkills;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Skills.Core;
using Microsoft.SemanticKernel.TemplateEngine;
using SemanticKernel.Service.CopilotChat.Extensions;
using SemanticKernel.Service.CopilotChat.Storage;
@@ -17,7 +17,7 @@
//
"Service": {
// "SemanticSkillsDirectory": "",
// "KeyVaultUri": ""
// "KeyVault": ""
},
//
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>

Some files were not shown because too many files have changed in this diff Show More