diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml
index 1189ed8098af..1b1a44fa1bb6 100644
--- a/.github/workflows/python-integration-tests.yml
+++ b/.github/workflows/python-integration-tests.yml
@@ -73,14 +73,28 @@ jobs:
- name: Install Ollama
if: matrix.os == 'ubuntu-latest'
run: |
- curl -fsSL https://ollama.com/install.sh | sh
- ollama serve &
- sleep 5
+ if ${{ vars.OLLAMA_MODEL != '' }}; then
+ curl -fsSL https://ollama.com/install.sh | sh
+ ollama serve &
+ sleep 5
+ fi
- name: Pull model in Ollama
if: matrix.os == 'ubuntu-latest'
run: |
- ollama pull ${{ vars.OLLAMA_MODEL }}
- ollama list
+ if ${{ vars.OLLAMA_MODEL != '' }}; then
+ ollama pull ${{ vars.OLLAMA_MODEL }}
+ ollama list
+ fi
+ - name: Google auth
+ uses: google-github-actions/auth@v2
+ with:
+ project_id: ${{ vars.VERTEX_AI_PROJECT_ID }}
+ credentials_json: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_KEY }}
+ - name: Set up gcloud
+ uses: google-github-actions/setup-gcloud@v2
+ - name: Setup Redis Stack Server
+ if: matrix.os == 'ubuntu-latest'
+ run: docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
- name: Run Integration Tests
id: run_tests
shell: bash
@@ -97,6 +111,7 @@ jobs:
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }}
OPENAI_TEXT_MODEL_ID: ${{ vars.OPENAI_TEXT_MODEL_ID }}
OPENAI_EMBEDDING_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
+ OPENAI_TEXT_TO_IMAGE_MODEL_ID: ${{ vars.OPENAI_TEXT_TO_IMAGE_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PINECONE_API_KEY: ${{ secrets.PINECONE__APIKEY }}
POSTGRES_CONNECTION_STRING: ${{secrets.POSTGRES__CONNECTIONSTR}}
@@ -109,18 +124,37 @@ jobs:
ACA_POOL_MANAGEMENT_ENDPOINT: ${{secrets.ACA_POOL_MANAGEMENT_ENDPOINT}}
MISTRALAI_API_KEY: ${{secrets.MISTRALAI_API_KEY}}
MISTRALAI_CHAT_MODEL_ID: ${{ vars.MISTRALAI_CHAT_MODEL_ID }}
+ MISTRALAI_EMBEDDING_MODEL_ID: ${{ vars.MISTRALAI_EMBEDDING_MODEL_ID }}
+ ANTHROPIC_API_KEY: ${{secrets.ANTHROPIC_API_KEY}}
+ ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
OLLAMA_MODEL: "${{ matrix.os == 'ubuntu-latest' && vars.OLLAMA_MODEL || '' }}" # phi3
GOOGLE_AI_GEMINI_MODEL_ID: ${{ vars.GOOGLE_AI_GEMINI_MODEL_ID }}
GOOGLE_AI_EMBEDDING_MODEL_ID: ${{ vars.GOOGLE_AI_EMBEDDING_MODEL_ID }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
+ VERTEX_AI_PROJECT_ID: ${{ vars.VERTEX_AI_PROJECT_ID }}
+ VERTEX_AI_GEMINI_MODEL_ID: ${{ vars.VERTEX_AI_GEMINI_MODEL_ID }}
+ VERTEX_AI_EMBEDDING_MODEL_ID: ${{ vars.VERTEX_AI_EMBEDDING_MODEL_ID }}
+ REDIS_CONNECTION_STRING: ${{ vars.REDIS_CONNECTION_STRING }}
run: |
- if ${{ matrix.os == 'ubuntu-latest' }}; then
- docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
- fi
-
cd python
- poetry run pytest ./tests/integration -v
- poetry run pytest ./tests/samples -v
+ poetry run pytest -n logical --dist loadfile --dist worksteal ./tests/integration ./tests/samples -v --junitxml=pytest.xml
+ - name: Surface failing tests
+ if: always()
+ uses: pmeier/pytest-results-action@main
+ with:
+ # A list of JUnit XML files, directories containing the former, and wildcard
+ # patterns to process.
+ # See @actions/glob for supported patterns.
+ path: python/pytest.xml
+ # (Optional) Add a summary of the results at the top of the report
+ summary: true
+ # (Optional) Select which results should be included in the report.
+ # Follows the same syntax as `pytest -r`
+ display-options: fEX
+ # (Optional) Fail the workflow if no JUnit XML was found.
+ fail-on-empty: true
+ # (Optional) Title of the test results section in the workflow summary
+ title: Test results
python-integration-tests:
needs: paths-filter
@@ -167,6 +201,15 @@ jobs:
ollama pull ${{ vars.OLLAMA_MODEL }}
ollama list
+ - name: Google auth
+ uses: google-github-actions/auth@v2
+ with:
+ project_id: ${{ vars.VERTEX_AI_PROJECT_ID }}
+ credentials_json: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_KEY }}
+
+ - name: Set up gcloud
+ uses: google-github-actions/setup-gcloud@v2
+
- name: Run Integration Tests
id: run_tests
shell: bash
@@ -183,6 +226,7 @@ jobs:
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }}
OPENAI_TEXT_MODEL_ID: ${{ vars.OPENAI_TEXT_MODEL_ID }}
OPENAI_EMBEDDING_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
+ OPENAI_TEXT_TO_IMAGE_MODEL_ID: ${{ vars.OPENAI_TEXT_TO_IMAGE_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PINECONE_API_KEY: ${{ secrets.PINECONE__APIKEY }}
POSTGRES_CONNECTION_STRING: ${{secrets.POSTGRES__CONNECTIONSTR}}
@@ -195,18 +239,25 @@ jobs:
ACA_POOL_MANAGEMENT_ENDPOINT: ${{secrets.ACA_POOL_MANAGEMENT_ENDPOINT}}
MISTRALAI_API_KEY: ${{secrets.MISTRALAI_API_KEY}}
MISTRALAI_CHAT_MODEL_ID: ${{ vars.MISTRALAI_CHAT_MODEL_ID }}
+ MISTRALAI_EMBEDDING_MODEL_ID: ${{ vars.MISTRALAI_EMBEDDING_MODEL_ID }}
OLLAMA_MODEL: "${{ matrix.os == 'ubuntu-latest' && vars.OLLAMA_MODEL || '' }}" # phi3
GOOGLE_AI_GEMINI_MODEL_ID: ${{ vars.GOOGLE_AI_GEMINI_MODEL_ID }}
GOOGLE_AI_EMBEDDING_MODEL_ID: ${{ vars.GOOGLE_AI_EMBEDDING_MODEL_ID }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
+ VERTEX_AI_PROJECT_ID: ${{ vars.VERTEX_AI_PROJECT_ID }}
+ VERTEX_AI_GEMINI_MODEL_ID: ${{ vars.VERTEX_AI_GEMINI_MODEL_ID }}
+ VERTEX_AI_EMBEDDING_MODEL_ID: ${{ vars.VERTEX_AI_EMBEDDING_MODEL_ID }}
+ REDIS_CONNECTION_STRING: ${{ vars.REDIS_CONNECTION_STRING }}
+ ANTHROPIC_API_KEY: ${{secrets.ANTHROPIC_API_KEY}}
+ ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
run: |
if ${{ matrix.os == 'ubuntu-latest' }}; then
docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
fi
cd python
- poetry run pytest ./tests/integration -v
- poetry run pytest ./tests/samples -v
+ poetry run pytest -n logical --dist loadfile --dist worksteal ./tests/integration -v
+ poetry run pytest -n logical --dist loadfile --dist worksteal ./tests/samples -v
# This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed
python-integration-tests-check:
diff --git a/.github/workflows/python-test-coverage.yml b/.github/workflows/python-test-coverage.yml
index d61da4f022a2..7d3c14ce783b 100644
--- a/.github/workflows/python-test-coverage.yml
+++ b/.github/workflows/python-test-coverage.yml
@@ -10,10 +10,6 @@ on:
types:
- in_progress
-env:
- PYTHON_VERSION: "3.10"
- RUN_OS: ubuntu-latest
-
jobs:
python-tests-coverage:
runs-on: ubuntu-latest
@@ -27,13 +23,13 @@ jobs:
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ github.event.pull_request.head.sha }}
- check-name: 'Python Unit Tests (${{ env.PYTHON_VERSION }}, ${{ env.RUN_OS }}, false)'
+ check-name: 'Python Test Coverage'
repo-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
wait-interval: 90
allowed-conclusions: success
- uses: actions/checkout@v4
- name: Setup filename variables
- run: echo "FILE_ID=${{ github.event.number }}-${{ env.RUN_OS }}-${{ env.PYTHON_VERSION }}" >> $GITHUB_ENV
+ run: echo "FILE_ID=${{ github.event.number }}" >> $GITHUB_ENV
- name: Download coverage
uses: dawidd6/action-download-artifact@v3
with:
@@ -57,9 +53,9 @@ jobs:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
pytest-coverage-path: python-coverage.txt
coverage-path-prefix: "python/"
- title: "Python ${{ env.PYTHON_VERSION }} Test Coverage Report"
- badge-title: "Py${{ env.PYTHON_VERSION }} Test Coverage"
- junitxml-title: "Python ${{ env.PYTHON_VERSION }} Unit Test Overview"
+ title: "Python Test Coverage Report"
+ badge-title: "Python Test Coverage"
+ junitxml-title: "Python Unit Test Overview"
junitxml-path: pytest.xml
default-branch: "main"
- unique-id-for-comment: python-${{ env.PYTHON_VERSION }}
+ unique-id-for-comment: python-test-coverage
diff --git a/.github/workflows/python-unit-tests.yml b/.github/workflows/python-unit-tests.yml
index 8e34ad0e9b5f..4137270c3796 100644
--- a/.github/workflows/python-unit-tests.yml
+++ b/.github/workflows/python-unit-tests.yml
@@ -18,7 +18,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
experimental: [false]
include:
- - python-version: "3.13.0-beta.3"
+ - python-version: "3.13.0-beta.4"
os: "ubuntu-latest"
experimental: true
permissions:
@@ -28,8 +28,6 @@ jobs:
working-directory: python
steps:
- uses: actions/checkout@v4
- - name: Setup filename variables
- run: echo "FILE_ID=${{ github.event.number }}-${{ matrix.os }}-${{ matrix.python-version }}" >> $GITHUB_ENV
- name: Install poetry
run: pipx install poetry
- name: Set up Python ${{ matrix.python-version }}
@@ -40,8 +38,50 @@ jobs:
- name: Install dependencies
run: poetry install --with unit-tests
- name: Test with pytest
- run: poetry run pytest -q --junitxml=pytest.xml --cov=semantic_kernel --cov-report=term-missing:skip-covered ./tests/unit | tee python-coverage.txt
+ run: poetry run pytest --junitxml=pytest.xml ./tests/unit
+ - name: Surface failing tests
+ if: always()
+ uses: pmeier/pytest-results-action@main
+ with:
+ # A list of JUnit XML files, directories containing the former, and wildcard
+ # patterns to process.
+ # See @actions/glob for supported patterns.
+ path: python/pytest.xml
+ # (Optional) Add a summary of the results at the top of the report
+ summary: true
+ # (Optional) Select which results should be included in the report.
+ # Follows the same syntax as `pytest -r`
+ display-options: fEX
+ # (Optional) Fail the workflow if no JUnit XML was found.
+ fail-on-empty: true
+ # (Optional) Title of the test results section in the workflow summary
+ title: Test results
+ python-test-coverage:
+ name: Python Test Coverage
+ runs-on: [ubuntu-latest]
+ continue-on-error: true
+ permissions:
+ contents: write
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup filename variables
+ run: echo "FILE_ID=${{ github.event.number }}" >> $GITHUB_ENV
+ - name: Install poetry
+ run: pipx install poetry
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ cache: "poetry"
+ - name: Install dependencies
+ run: poetry install --with unit-tests
+ - name: Test with pytest
+ run: poetry run pytest -q --junitxml=pytest.xml --cov=semantic_kernel --cov-report=term-missing:skip-covered ./tests/unit | tee python-coverage.txt
- name: Upload coverage
+ if: always()
uses: actions/upload-artifact@v4
with:
name: python-coverage-${{ env.FILE_ID }}.txt
@@ -49,6 +89,7 @@ jobs:
overwrite: true
retention-days: 1
- name: Upload pytest.xml
+ if: always()
uses: actions/upload-artifact@v4
with:
name: pytest-${{ env.FILE_ID }}.xml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5b983bf90ade..5fd6aa7d9377 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,7 +37,7 @@ repos:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.5.2
+ rev: v0.5.7
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
diff --git a/README.md b/README.md
index 215e13a43cce..2cc88b643a4a 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,13 @@
## Status
- - Python
-[![Python package](https://img.shields.io/pypi/v/semantic-kernel)](https://pypi.org/project/semantic-kernel/)
- - .NET
-[![Nuget package](https://img.shields.io/nuget/vpre/Microsoft.SemanticKernel)](https://www.nuget.org/packages/Microsoft.SemanticKernel/)[![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)
+- Python
+ [![Python package](https://img.shields.io/pypi/v/semantic-kernel)](https://pypi.org/project/semantic-kernel/)
+- .NET
+ [![Nuget package](https://img.shields.io/nuget/vpre/Microsoft.SemanticKernel)](https://www.nuget.org/packages/Microsoft.SemanticKernel/)[![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)
## Overview
+
[![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)
@@ -27,8 +28,8 @@ plugins with AI. With Semantic Kernel
can ask an LLM to generate a plan that achieves a user's unique goal. Afterwards,
Semantic Kernel will execute the plan for the user.
-
It provides:
+
- abstractions for AI services (such as chat, text to images, audio to text, etc.) and memory stores
- implementations of those abstractions for services from [OpenAI](https://platform.openai.com/docs/introduction), [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service), [Hugging Face](https://huggingface.co/), local models, and more, and for a multitude of vector databases, such as those from [Chroma](https://docs.trychroma.com/getting-started), [Qdrant](https://qdrant.tech/), [Milvus](https://milvus.io/), and [Azure](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search)
- a common representation for [plugins](https://learn.microsoft.com/en-us/semantic-kernel/ai-orchestration/plugins), which can then be orchestrated automatically by AI
@@ -45,7 +46,7 @@ Semantic Kernel was designed to be future proof, easily connecting your code to
## Getting started with Semantic Kernel
-The Semantic Kernel SDK is available in C#, Python, and Java. To get started, choose your preferred language below. See the [Feature Matrix](https://learn.microsoft.com/en-us/semantic-kernel/get-started/supported-languages) to see a breakdown of
+The Semantic Kernel SDK is available in C#, Python, and Java. To get started, choose your preferred language below. See the [Feature Matrix](https://learn.microsoft.com/en-us/semantic-kernel/get-started/supported-languages) for a breakdown of
feature parity between our currently supported languages.
@@ -84,9 +85,9 @@ from either OpenAI or Azure OpenAI and to run one of the C#, Python, and Java co
### For Python:
-1. Go to the Quick start page [here](https://learn.microsoft.com/en-us/semantic-kernel/get-started/quick-start-guide?pivots=programming-language-csharp) and follow the steps to dive in.
-2. You'll need to ensure that you toggle to C# in the the Choose a programming language table at the top of the page.
- ![csharpmap](https://learn.microsoft.com/en-us/semantic-kernel/media/pythonmap.png)
+1. Go to the Quick start page [here](https://learn.microsoft.com/en-us/semantic-kernel/get-started/quick-start-guide?pivots=programming-language-python) and follow the steps to dive in.
+2. You'll need to ensure that you toggle to Python in the the Choose a programming language table at the top of the page.
+ ![pythonmap](https://learn.microsoft.com/en-us/semantic-kernel/media/pythonmap.png)
### For Java:
@@ -115,10 +116,11 @@ on our Learn site. Each sample comes with a completed C# and Python project that
Finally, refer to our API references for more details on the C# and Python APIs:
- [C# API reference](https://learn.microsoft.com/en-us/dotnet/api/microsoft.semantickernel?view=semantic-kernel-dotnet)
-- Python API reference (coming soon)
+- [Python API reference](https://learn.microsoft.com/en-us/python/api/semantic-kernel/semantic_kernel?view=semantic-kernel-python)
- Java API reference (coming soon)
## Visual Studio Code extension: design semantic functions with ease
+
The Semantic Kernel extension for Visual Studio Code makes it easy to design and test semantic functions. The extension provides an interface for designing semantic functions and allows you to test them with the push of a button with your existing models and data.
## Join the community
diff --git a/TRANSPARENCY_FAQS.md b/TRANSPARENCY_FAQS.md
new file mode 100644
index 000000000000..a891ec68ec28
--- /dev/null
+++ b/TRANSPARENCY_FAQS.md
@@ -0,0 +1,70 @@
+# Semantic Kernel Responsible AI FAQs
+
+## What is Microsoft Semantic Kernel?
+Microsoft Semantic Kernel is a lightweight, open-source development kit designed to facilitate the integration of AI models into applications written in languages such as C#, Python, or Java.
+
+It serves as efficient middleware that supports developers in building AI agents, automating business processes, and connecting their code with the latest AI technologies. Input to this system can range from text data to structured commands, and it produces various outputs, including natural language responses, function calls, and other actionable data.
+
+
+## What can Microsoft Semantic Kernel do?
+Building upon its foundational capabilities, Microsoft Semantic Kernel facilitates several functionalities:
+- AI Agent Development: Users can create agents capable of performing specific tasks or interactions based on user input.
+- Function Invocation: It can automate code execution by calling functions based on AI model outputs.
+- Modular and Extensible: Developers can enhance functionality through plugins and a variety of pre-built connectors, providing flexibility in integrating additional AI services.
+- Multi-Modal Support: The kernel easily expands existing applications to support modalities like voice and video through its architecture
+- Filtering: Developers can use filters to monitor the application, control function invocation or implement Responsible AI.
+- Prompt Templates: Developer can define their prompts using various template languages including Handlebars and Liquid or the built-in Semantic Kernel format.
+
+
+## What is/are Microsoft Semantic Kernel’s intended use(s)?
+The intended uses of Microsoft Semantic Kernel include:
+- Production Ready Applications: Building small to large enterprise scale solutions that can leverage advanced AI models capabilities.
+- Automation of Business Processes: Facilitating quick and efficient automation of workflows and tasks within organizations.
+- Integration of AI Services: Connecting client code with a variety of pre-built AI services and capabilities for rapid development.
+
+
+## How was Microsoft Semantic Kernel evaluated? What metrics are used to measure performance?
+Microsoft Semantic Kernel was reviewed for reliability and performance metrics that include:
+- Accuracy: Evaluated based on the correctness of the outputs generated against known facts.
+- Integration Speed: Assessed by the time taken to integrate AI models and initiate functional outputs based on telemetry.
+- Performance Consistency: Measurements taken to verify the system's reliability based on telemetry.
+
+
+## What are the limitations of Microsoft Semantic Kernel?
+Semantic Kernel integrates with Large Language Models (LLMs) to allow AI capabilities to be added to existing application.
+LLMs have some inherent limitations such as:
+- Contextual Misunderstanding: The system may struggle with nuanced requests, particularly those involving complex context.
+- Bias in LLM Outputs: Historical biases in the training data can inadvertently influence model outputs.
+ - Users can mitigate these issues by:
+ - Formulating clear and explicit queries.
+ - Regularly reviewing AI-generated outputs to identify and rectify biases or inaccuracies.
+ - Providing relevant information when prompting the LLM so that it can base it's responses on this data
+- Not all LLMs support all features uniformly e.g., function calling.
+Semantic Kernel is constantly evolving and adding new features so:
+- There are some components still being developed e.g., support for some modalities such as Video and Classification, memory connectors for certain Vector databases, AI connectors for certain AI services.
+- There are some components that are still experimental, these are clearly flagged and are subject to change.
+
+## What operational factors and settings allow for effective and responsible use of Microsoft Semantic Kernel?
+Operational factors and settings for optimal use include:
+- Custom Configuration Options: Users can tailor system parameters to match specific application needs, such as output style or verbosity.
+- Safe Operating Parameters: The system operates best within defined ranges of input complexity and length, ensuring reliability and safety.
+- Real-Time Monitoring: System behavior should be regularly monitored to detect unexpected patterns or malfunctions promptly.
+- Incorporate RAI and safety tools like Prompt Shield with filters to ensure responsible use.
+
+
+### Plugins and Extensibility
+
+#### What are plugins and how does Microsoft Semantic Kernel use them?
+Plugins are API calls that enhance and extend the capabilities of Microsoft Semantic Kernel by integrating with other services. They can be developed internally or by third-party developers, offering functionalities that users can toggle on or off based on their requirements. The kernel supports OpenAPI specifications, allowing for easy integration and sharing of plugins within developer teams.
+
+#### What data can Microsoft Semantic Kernel provide to plugins? What permissions do Microsoft Semantic Kernel plugins have?
+Plugins can access essential user information necessary for their operation, such as:
+- Input Context: Information directly related to the queries and commands issued to the system.
+- Execution Data: Results and performance metrics from previous operations, provided they adhere to user privacy standards. Developers retain control over plugin permissions, choosing what information plugins can access or transmit, ensuring compliance with data protection protocols.
+- Semantic Kernel supports filters which allow developers to integrate with RAI solutions
+
+#### What kinds of issues may arise when using Microsoft Semantic Kernel enabled with plugins?
+Potential issues that may arise include:
+- Invocation Failures: Incorrectly triggered plugins can result in unexpected outputs.
+- Output Misinformation: Errors in plugin handling can lead to generation of inaccurate or misleading results.
+- Dependency Compatibility: Changes in external dependencies may affect plugin functionality. To prevent these issues, users are advised to keep plugins updated and to rigorously test their implementations for stability and accuracy
diff --git a/docs/decisions/0050-updated-vector-store-design.md b/docs/decisions/0050-updated-vector-store-design.md
new file mode 100644
index 000000000000..c008068b1e95
--- /dev/null
+++ b/docs/decisions/0050-updated-vector-store-design.md
@@ -0,0 +1,995 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: proposed
+contact: westey-m
+date: 2024-06-05
+deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, matthewbolanos, eavanvalkenburg
+consulted: stephentoub, dluc, ajcvickers, roji
+informed:
+---
+
+# Updated Memory Connector Design
+
+## Context and Problem Statement
+
+Semantic Kernel has a collection of connectors to popular Vector databases e.g. Azure AI Search, Chroma, Milvus, ...
+Each Memory connector implements a memory abstraction defined by Semantic Kernel and allows developers to easily integrate Vector databases into their applications.
+The current abstractions are experimental and the purpose of this ADR is to progress the design of the abstractions so that they can graduate to non experimental status.
+
+### Problems with current design
+
+1. The `IMemoryStore` interface has four responsibilities with different cardinalities. Some are schema aware and others schema agnostic.
+2. The `IMemoryStore` interface only supports a fixed schema for data storage, retrieval and search, which limits its usability by customers with existing data sets.
+2. The `IMemoryStore` implementations are opinionated around key encoding / decoding and collection name sanitization, which limits its usability by customers with existing data sets.
+
+Responsibilities:
+
+|Functional Area|Cardinality|Significance to Semantic Kernel|
+|-|-|-|
+|Collection/Index create|An implementation per store type and model|Valuable when building a store and adding data|
+|Collection/Index list names, exists and delete|An implementation per store type|Valuable when building a store and adding data|
+|Data Storage and Retrieval|An implementation per store type|Valuable when building a store and adding data|
+|Vector Search|An implementation per store type, model and search type|Valuable for many scenarios including RAG, finding contradictory facts based on user input, finding similar memories to merge, etc.|
+
+
+### Memory Store Today
+```cs
+interface IMemoryStore
+{
+ // Collection / Index Management
+ Task CreateCollectionAsync(string collectionName, CancellationToken cancellationToken = default);
+ IAsyncEnumerable GetCollectionsAsync(CancellationToken cancellationToken = default);
+ Task DoesCollectionExistAsync(string collectionName, CancellationToken cancellationToken = default);
+ Task DeleteCollectionAsync(string collectionName, CancellationToken cancellationToken = default);
+
+ // Data Storage and Retrieval
+ Task UpsertAsync(string collectionName, MemoryRecord record, CancellationToken cancellationToken = default);
+ IAsyncEnumerable UpsertBatchAsync(string collectionName, IEnumerable records, CancellationToken cancellationToken = default);
+ Task GetAsync(string collectionName, string key, bool withEmbedding = false, CancellationToken cancellationToken = default);
+ IAsyncEnumerable GetBatchAsync(string collectionName, IEnumerable keys, bool withVectors = false, CancellationToken cancellationToken = default);
+ Task RemoveAsync(string collectionName, string key, CancellationToken cancellationToken = default);
+ Task RemoveBatchAsync(string collectionName, IEnumerable keys, CancellationToken cancellationToken = default);
+
+ // Vector Search
+ IAsyncEnumerable<(MemoryRecord, double)> GetNearestMatchesAsync(
+ string collectionName,
+ ReadOnlyMemory embedding,
+ int limit,
+ double minRelevanceScore = 0.0,
+ bool withVectors = false,
+ CancellationToken cancellationToken = default);
+
+ Task<(MemoryRecord, double)?> GetNearestMatchAsync(
+ string collectionName,
+ ReadOnlyMemory embedding,
+ double minRelevanceScore = 0.0,
+ bool withEmbedding = false,
+ CancellationToken cancellationToken = default);
+}
+```
+
+### Actions
+
+1. The `IMemoryStore` should be split into different interfaces, so that schema aware and schema agnostic operations are separated.
+2. The **Data Storage and Retrieval** and **Vector Search** areas should allow typed access to data and support any schema that is currently available in the customer's data store.
+3. The collection / index create functionality should allow developers to use a common definition that is part of the abstraction to create collections.
+4. The collection / index list/exists/delete functionality should allow management of any collection regardless of schema.
+5. Remove opinionated behaviors from connectors. The opinionated behavior limits the ability of these connectors to be used with pre-existing vector databases. As far as possible these behaviors should be moved into decorators or be injectable. Examples of opinionated behaviors:
+ 1. The AzureAISearch connector encodes keys before storing and decodes them after retrieval since keys in Azure AI Search supports a limited set of characters.
+ 2. The AzureAISearch connector sanitizes collection names before using them, since Azure AI Search supports a limited set of characters.
+ 3. The Redis connector prepends the collection name on to the front of keys before storing records and also registers the collection name as a prefix for records to be indexed by the index.
+
+### Non-functional requirements for new connectors
+1. Ensure all connectors are throwing the same exceptions consistently with data about the request made provided in a consistent manner.
+2. Add consistent telemetry for all connectors.
+3. As far as possible integration tests should be runnable on build server.
+
+### New Designs
+
+The separation between collection/index management and record management.
+
+```mermaid
+---
+title: SK Collection/Index and record management
+---
+classDiagram
+ note for IVectorRecordStore "Can manage records for any scenario"
+ note for IVectorCollectionCreate "Can create collections and\nindexes"
+ note for IVectorCollectionNonSchema "Can retrieve/delete any collections and\nindexes"
+
+ namespace SKAbstractions{
+ class IVectorCollectionCreate{
+ <>
+ +CreateCollection
+ }
+
+ class IVectorCollectionNonSchema{
+ <>
+ +GetCollectionNames
+ +CollectionExists
+ +DeleteCollection
+ }
+
+ class IVectorRecordStore~TModel~{
+ <>
+ +Upsert(TModel record) string
+ +UpserBatch(TModel record) string
+ +Get(string key) TModel
+ +GetBatch(string[] keys) TModel[]
+ +Delete(string key)
+ +DeleteBatch(string[] keys)
+ }
+ }
+
+ namespace AzureAIMemory{
+ class AzureAISearchVectorCollectionCreate{
+ }
+
+ class AzureAISearchVectorCollectionNonSchema{
+ }
+
+ class AzureAISearchVectorRecordStore{
+ }
+ }
+
+ namespace RedisMemory{
+ class RedisVectorCollectionCreate{
+ }
+
+ class RedisVectorCollectionNonSchema{
+ }
+
+ class RedisVectorRecordStore{
+ }
+ }
+
+ IVectorCollectionCreate <|-- AzureAISearchVectorCollectionCreate
+ IVectorCollectionNonSchema <|-- AzureAISearchVectorCollectionNonSchema
+ IVectorRecordStore <|-- AzureAISearchVectorRecordStore
+
+ IVectorCollectionCreate <|-- RedisVectorCollectionCreate
+ IVectorCollectionNonSchema <|-- RedisVectorCollectionNonSchema
+ IVectorRecordStore <|-- RedisVectorRecordStore
+```
+
+How to use your own schema with core sk functionality.
+
+```mermaid
+---
+title: Chat History Break Glass
+---
+classDiagram
+ note for IVectorRecordStore "Can manage records\nfor any scenario"
+ note for IVectorCollectionCreate "Can create collections\nan dindexes"
+ note for IVectorCollectionNonSchema "Can retrieve/delete any\ncollections and indexes"
+ note for CustomerHistoryVectorCollectionCreate "Creates history collections and indices\nusing Customer requirements"
+ note for CustomerHistoryVectorRecordStore "Decorator class for IVectorRecordStore that maps\nbetween the customer model to our model"
+
+ namespace SKAbstractions{
+ class IVectorCollectionCreate{
+ <>
+ +CreateCollection
+ }
+
+ class IVectorCollectionNonSchema{
+ <>
+ +GetCollectionNames
+ +CollectionExists
+ +DeleteCollection
+ }
+
+ class IVectorRecordStore~TModel~{
+ <>
+ +Upsert(TModel record) string
+ +Get(string key) TModel
+ +Delete(string key) string
+ }
+
+ class ISemanticTextMemory{
+ <>
+ +SaveInformationAsync()
+ +SaveReferenceAsync()
+ +GetAsync()
+ +DeleteAsync()
+ +SearchAsync()
+ +GetCollectionsAsync()
+ }
+ }
+
+ namespace CustomerProject{
+ class CustomerHistoryModel{
+ +string text
+ +float[] vector
+ +Dictionary~string, string~ properties
+ }
+
+ class CustomerHistoryVectorCollectionCreate{
+ +CreateCollection
+ }
+
+ class CustomerHistoryVectorRecordStore{
+ -IVectorRecordStore~CustomerHistoryModel~ _store
+ +Upsert(ChatHistoryModel record) string
+ +Get(string key) ChatHistoryModel
+ +Delete(string key) string
+ }
+ }
+
+ namespace SKCore{
+ class SemanticTextMemory{
+ -IVectorRecordStore~ChatHistoryModel~ _VectorRecordStore
+ -IMemoryCollectionService _collectionsService
+ -ITextEmbeddingGenerationService _embeddingGenerationService
+ }
+
+ class ChatHistoryPlugin{
+ -ISemanticTextMemory memory
+ }
+
+ class ChatHistoryModel{
+ +string message
+ +float[] embedding
+ +Dictionary~string, string~ metadata
+ }
+ }
+
+ IVectorCollectionCreate <|-- CustomerHistoryVectorCollectionCreate
+
+ IVectorRecordStore <|-- CustomerHistoryVectorRecordStore
+ IVectorRecordStore <.. CustomerHistoryVectorRecordStore
+ CustomerHistoryModel <.. CustomerHistoryVectorRecordStore
+ ChatHistoryModel <.. CustomerHistoryVectorRecordStore
+
+ ChatHistoryModel <.. SemanticTextMemory
+ IVectorRecordStore <.. SemanticTextMemory
+ IVectorCollectionCreate <.. SemanticTextMemory
+
+ ISemanticTextMemory <.. ChatHistoryPlugin
+```
+
+### Vector Store Cross Store support - General Features
+
+A comparison of the different ways in which stores implement storage capabilities to help drive decisions:
+
+|Feature|Azure AI Search|Weaviate|Redis|Chroma|FAISS|Pinecone|LLamaIndex|PostgreSql|Qdrant|Milvus|
+|-|-|-|-|-|-|-|-|-|-|-|
+|Get Item Support|Y|Y|Y|Y||Y||Y|Y|Y|
+|Batch Operation Support|Y|Y|Y|Y||Y||||Y|
+|Per Item Results for Batch Operations|Y|Y|Y|N||N|||||
+|Keys of upserted records|Y|Y|N3|N3||N3||||Y|
+|Keys of removed records|Y||N3|N||N||||N3|
+|Retrieval field selection for gets|Y||Y4|P2||N||Y|Y|Y|
+|Include/Exclude Embeddings for gets|P1|Y|Y4,1|Y||N||P1|Y|N|
+|Failure reasons when batch partially fails|Y|Y|Y|N||N|||||
+|Is Key separate from data|N|Y|Y|Y||Y||N|Y|N|
+|Can Generate Ids|N|Y|N|N||Y||Y|N|Y|
+|Can Generate Embedding|Not Available Via API yet|Y|N|Client Side Abstraction|||||N||
+
+Footnotes:
+- P = Partial Support
+- 1 Only if you have the schema, to select the appropriate fields.
+- 2 Supports broad categories of fields only.
+- 3 Id is required in request, so can be returned if needed.
+- 4 No strong typed support when specifying field list.
+
+### Vector Store Cross Store support - Fields, types and indexing
+
+|Feature|Azure AI Search|Weaviate|Redis|Chroma|FAISS|Pinecone|LLamaIndex|PostgreSql|Qdrant|Milvus|
+|-|-|-|-|-|-|-|-|-|-|-|
+|Field Differentiation|Fields|Key, Props, Vectors|Key, Fields|Key, Document, Metadata, Vector||Key, Metadata, SparseValues, Vector||Fields|Key, Props(Payload), Vectors|Fields|
+|Multiple Vector per record support|Y|Y|Y|N||[N](https://docs.pinecone.io/guides/data/upsert-data#upsert-records-with-metadata)||Y|Y|Y|
+|Index to Collection|1 to 1|1 to 1|1 to many|1 to 1|-|1 to 1|-|1 to 1|1 to 1|1 to 1|
+|Id Type|String|UUID|string with collection name prefix|string||string|UUID|64Bit Int / UUID / ULID|64Bit Unsigned Int / UUID|Int64 / varchar|
+|Supported Vector Types|[Collection(Edm.Byte) / Collection(Edm.Single) / Collection(Edm.Half) / Collection(Edm.Int16) / Collection(Edm.SByte)](https://learn.microsoft.com/en-us/rest/api/searchservice/supported-data-types)|float32|FLOAT32 and FLOAT64|||[Rust f32](https://docs.pinecone.io/troubleshooting/embedding-values-changed-when-upserted)||[single-precision (4 byte float) / half-precision (2 byte float) / binary (1bit) / sparse vectors (4 bytes)](https://github.com/pgvector/pgvector?tab=readme-ov-file#pgvector)|UInt8 / Float32|Binary / Float32 / Float16 / BFloat16 / SparseFloat|
+|Supported Distance Functions|[Cosine / dot prod / euclidean dist (l2 norm)](https://learn.microsoft.com/en-us/azure/search/vector-search-ranking#similarity-metrics-used-to-measure-nearness)|[Cosine dist / dot prod / Squared L2 dist / hamming (num of diffs) / manhattan dist](https://weaviate.io/developers/weaviate/config-refs/distances#available-distance-metrics)|[Euclidean dist (L2) / Inner prod (IP) / Cosine dist](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/vectors/)|[Squared L2 / Inner prod / Cosine similarity](https://docs.trychroma.com/guides#changing-the-distance-function)||[cosine sim / euclidean dist / dot prod](https://docs.pinecone.io/reference/api/control-plane/create_index)||[L2 dist / inner prod / cosine dist / L1 dist / Hamming dist / Jaccard dist (NB: Specified at query time, not index creation time)](https://github.com/pgvector/pgvector?tab=readme-ov-file#pgvector)|[Dot prod / Cosine sim / Euclidean dist (L2) / Manhattan dist](https://qdrant.tech/documentation/concepts/search/)|[Cosine sim / Euclidean dist / Inner Prod](https://milvus.io/docs/index-vector-fields.md)|
+|Supported index types|[Exhaustive KNN (FLAT) / HNSW](https://learn.microsoft.com/en-us/azure/search/vector-search-ranking#algorithms-used-in-vector-search)|[HNSW / Flat / Dynamic](https://weaviate.io/developers/weaviate/config-refs/schema/vector-index)|[HNSW / FLAT](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/vectors/#create-a-vector-field)|[HNSW not configurable](https://cookbook.chromadb.dev/core/concepts/#vector-index-hnsw-index)||[PGA](https://www.pinecone.io/blog/hnsw-not-enough/)||[HNSW / IVFFlat](https://github.com/pgvector/pgvector?tab=readme-ov-file#indexing)|[HNSW for dense](https://qdrant.tech/documentation/concepts/indexing/#vector-index)|
|
+
+Footnotes:
+- HNSW = Hierarchical Navigable Small World (HNSW performs an [approximate nearest neighbor (ANN)](https://learn.microsoft.com/en-us/azure/search/vector-search-overview#approximate-nearest-neighbors) search)
+- KNN = k-nearest neighbors (performs a brute-force search that scans the entire vector space)
+- IVFFlat = Inverted File with Flat Compression (This index type uses approximate nearest neighbor search (ANNS) to provide fast searches)
+- Weaviate Dynamic = Starts as flat and switches to HNSW if the number of objects exceed a limit
+- PGA = [Pinecone Graph Algorithm](https://www.pinecone.io/blog/hnsw-not-enough/)
+
+### Vector Store Cross Store support - Search and filtering
+
+|Feature|Azure AI Search|Weaviate|Redis|Chroma|FAISS|Pinecone|LLamaIndex|PostgreSql|Qdrant|Milvus|
+|-|-|-|-|-|-|-|-|-|-|-|
+|Index allows text search|Y|Y|Y|Y (On Metadata by default)||[Only in combination with Vector](https://docs.pinecone.io/guides/data/understanding-hybrid-search)||Y (with TSVECTOR field)|Y|Y|
+|Text search query format|[Simple or Full Lucene](https://learn.microsoft.com/en-us/azure/search/search-query-create?tabs=portal-text-query#choose-a-query-type-simple--full)|[wildcard](https://weaviate.io/developers/weaviate/search/filters#filter-text-on-partial-matches)|wildcard & fuzzy|[contains & not contains](https://docs.trychroma.com/guides#filtering-by-document-contents)||Text only||[wildcard & binary operators](https://www.postgresql.org/docs/16/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES)|[Text only](https://qdrant.tech/documentation/concepts/filtering/#full-text-match)|[wildcard](https://milvus.io/docs/single-vector-search.md#Filtered-search)|
+|Multi Field Vector Search Support|Y|[N](https://weaviate.io/developers/weaviate/search/similarity)||N (no multi vector support)||N||[Unclear due to order by syntax](https://github.com/pgvector/pgvector?tab=readme-ov-file#querying)|[N](https://qdrant.tech/documentation/concepts/search/)|[Y](https://milvus.io/api-reference/restful/v2.4.x/v2/Vector%20(v2)/Hybrid%20Search.md)|
+|Targeted Multi Field Text Search Support|Y|[Y](https://weaviate.io/developers/weaviate/search/hybrid#set-weights-on-property-values)|[Y](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/query_syntax/#field-modifiers)|N (only on document)||N||Y|Y|Y|
+|Vector per Vector Field for Search|Y|N/A||N/A|||N/A||N/A|N/A|[Y](https://milvus.io/docs/multi-vector-search.md#Step-1-Create-Multiple-AnnSearchRequest-Instances)|
+|Separate text search query from vectors|Y|[Y](https://weaviate.io/developers/weaviate/search/hybrid#specify-a-search-vector)|Y|Y||Y||Y|Y|[Y](https://milvus.io/api-reference/restful/v2.4.x/v2/Vector%20(v2)/Hybrid%20Search.md)|
+|Allows filtering|Y|Y|Y (on TAG)|Y (On Metadata by default)||[Y](https://docs.pinecone.io/guides/indexes/configure-pod-based-indexes#selective-metadata-indexing)||Y|Y|Y|
+|Allows filter grouping|Y (Odata)|[Y](https://weaviate.io/developers/weaviate/search/filters#nested-filters)||[Y](https://docs.trychroma.com/guides#using-logical-operators)||Y||Y|[Y](https://qdrant.tech/documentation/concepts/filtering/#clauses-combination)|[Y](https://milvus.io/docs/get-and-scalar-query.md#Use-Basic-Operators)|
+|Allows scalar index field setup|Y|Y|Y|N||Y||Y|Y|Y|
+|Requires scalar index field setup to filter|Y|Y|Y|N||N (on by default for all)||N|N|N (can filter without index)|
+
+### Support for different mappers
+
+Mapping between data models and the storage models can also require custom logic depending on the type of data model and storage model involved.
+
+I'm therefore proposing that we allow mappers to be injectable for each `VectorStoreCollection` instance. The interfaces for these would vary depending
+on the storage models used by each vector store and any unique capabilities that each vector store may have, e.g. qdrant can operate in `single` or
+`multiple named vector` modes, which means the mapper needs to know whether to set a single vector or fill a vector map.
+
+In addition to this, we should build first party mappers for each of the vector stores, which will cater for built in, generic models or use metadata to perform the mapping.
+
+### Support for different storage schemas
+
+The different stores vary in many ways around how data is organized.
+- Some just store a record with fields on it, where fields can be a key or a data field or a vector and their type is determined at collection creation time.
+- Others separate fields by type when interacting with the api, e.g. you have to specify a key explicitly, put metadata into a metadata dictionary and put vectors into a vector array.
+
+I'm proposing that we allow two ways in which to provide the information required to map data between the consumer data model and storage data model.
+First is a set of configuration objects that capture the types of each field. Second would be a set of attributes that can be used to decorate the model itself
+and can be converted to the configuration objects, allowing a single execution path.
+Additional configuration properties can easily be added for each type of field as required, e.g. IsFilterable or IsFullTextSearchable, allowing us to also create an index from the provided configuration.
+
+I'm also proposing that even though similar attributes already exist in other systems, e.g. System.ComponentModel.DataAnnotations.KeyAttribute, we create our own.
+We will likely require additional properties on all these attributes that are not currently supported on the existing attributes, e.g. whether a field is or
+should be filterable. Requiring users to switch to new attributes later will be disruptive.
+
+Here is what the attributes would look like, plus a sample use case.
+
+```cs
+sealed class VectorStoreRecordKeyAttribute : Attribute
+{
+}
+sealed class VectorStoreRecordDataAttribute : Attribute
+{
+ public bool HasEmbedding { get; set; }
+ public string EmbeddingPropertyName { get; set; }
+}
+sealed class VectorStoreRecordVectorAttribute : Attribute
+{
+}
+
+public record HotelInfo(
+ [property: VectorStoreRecordKey, JsonPropertyName("hotel-id")] string HotelId,
+ [property: VectorStoreRecordData, JsonPropertyName("hotel-name")] string HotelName,
+ [property: VectorStoreRecordData(HasEmbedding = true, EmbeddingPropertyName = "DescriptionEmbeddings"), JsonPropertyName("description")] string Description,
+ [property: VectorStoreRecordVector, JsonPropertyName("description-embeddings")] ReadOnlyMemory? DescriptionEmbeddings);
+```
+
+Here is what the configuration objects would look like.
+
+```cs
+abstract class VectorStoreRecordProperty(string propertyName);
+
+sealed class VectorStoreRecordKeyProperty(string propertyName): Field(propertyName)
+{
+}
+sealed class VectorStoreRecordDataProperty(string propertyName): Field(propertyName)
+{
+ bool HasEmbedding;
+ string EmbeddingPropertyName;
+}
+sealed class VectorStoreRecordVectorProperty(string propertyName): Field(propertyName)
+{
+}
+
+sealed class VectorStoreRecordDefinition
+{
+ IReadOnlyList Properties;
+}
+```
+
+### Notable method signature changes from existing interface
+
+All methods currently existing on IMemoryStore will be ported to new interfaces, but in places I am proposing that we make changes to improve
+consistency and scalability.
+
+1. `RemoveAsync` and `RemoveBatchAsync` renamed to `DeleteAsync` and `DeleteBatchAsync`, since record are actually deleted, and this also matches the verb used for collections.
+2. `GetCollectionsAsync` renamed to `GetCollectionNamesAsync`, since we are only retrieving names and no other information about collections.
+3. `DoesCollectionExistAsync` renamed to `CollectionExistsAsync` since this is shorter and is more commonly used in other apis.
+
+### Comparison with other AI frameworks
+
+|Criteria|Current SK Implementation|Proposed SK Implementation|Spring AI|LlamaIndex|Langchain|
+|-|-|-|-|-|-|
+|Support for Custom Schemas|N|Y|N|N|N|
+|Naming of store|MemoryStore|VectorStore, VectorStoreCollection|VectorStore|VectorStore|VectorStore|
+|MultiVector support|N|Y|N|N|N|
+|Support Multiple Collections via SDK params|Y|Y|N (via app config)|Y|Y|
+
+## Decision Drivers
+
+From GitHub Issue:
+- API surface must be easy to use and intuitive
+- Alignment with other patterns in the SK
+- - Design must allow Memory Plugins to be easily instantiated with any connector
+- Design must support all Kernel content types
+- Design must allow for database specific configuration
+- All NFR's to be production ready are implemented (see Roadmap for more detail)
+- Basic CRUD operations must be supported so that connectors can be used in a polymorphic manner
+- Official Database Clients must be used where available
+- Dynamic database schema must be supported
+- Dependency injection must be supported
+- Azure-ML YAML format must be supported
+- Breaking glass scenarios must be supported
+
+## Considered Questions
+
+1. Combined collection and record management vs separated.
+2. Collection name and key value normalization in decorator or main class.
+3. Collection name as method param or constructor param.
+4. How to normalize ids across different vector stores where different types are supported.
+5. Store Interface/Class Naming
+
+### Question 1: Combined collection and record management vs separated.
+
+#### Option 1 - Combined collection and record management
+
+```cs
+interface IVectorRecordStore
+{
+ Task CreateCollectionAsync(CollectionCreateConfig collectionConfig, CancellationToken cancellationToken = default);
+ IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default);
+ Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default);
+ Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default);
+
+ Task UpsertAsync(TRecord data, CancellationToken cancellationToken = default);
+ IAsyncEnumerable UpsertBatchAsync(IEnumerable dataSet, CancellationToken cancellationToken = default);
+ Task GetAsync(string key, bool withEmbedding = false, CancellationToken cancellationToken = default);
+ IAsyncEnumerable GetBatchAsync(IEnumerable keys, bool withVectors = false, CancellationToken cancellationToken = default);
+ Task DeleteAsync(string key, CancellationToken cancellationToken = default);
+ Task DeleteBatchAsync(IEnumerable keys, CancellationToken cancellationToken = default);
+}
+
+class AzureAISearchVectorRecordStore(
+ Azure.Search.Documents.Indexes.SearchIndexClient client,
+ Schema schema): IVectorRecordStore;
+
+class WeaviateVectorRecordStore(
+ WeaviateClient client,
+ Schema schema): IVectorRecordStore;
+
+class RedisVectorRecordStore(
+ StackExchange.Redis.IDatabase database,
+ Schema schema): IVectorRecordStore;
+```
+
+#### Option 2 - Separated collection and record management with opinionated create implementations
+
+```cs
+
+interface IVectorCollectionStore
+{
+ virtual Task CreateChatHistoryCollectionAsync(string name, CancellationToken cancellationToken = default);
+ virtual Task CreateSemanticCacheCollectionAsync(string name, CancellationToken cancellationToken = default);
+
+ IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default);
+ Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default);
+ Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default);
+}
+
+class AzureAISearchVectorCollectionStore: IVectorCollectionStore;
+class RedisVectorCollectionStore: IVectorCollectionStore;
+class WeaviateVectorCollectionStore: IVectorCollectionStore;
+
+// Customers can inherit from our implementations and replace just the creation scenarios to match their schemas.
+class CustomerCollectionStore: AzureAISearchVectorCollectionStore, IVectorCollectionStore;
+
+// We can also create implementations that create indices based on an MLIndex specification.
+class MLIndexAzureAISearchVectorCollectionStore(MLIndex mlIndexSpec): AzureAISearchVectorCollectionStore, IVectorCollectionStore;
+
+interface IVectorRecordStore
+{
+ Task GetAsync(string key, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
+ Task DeleteAsync(string key, DeleteRecordOptions? options = default, CancellationToken cancellationToken = default);
+ Task UpsertAsync(TRecord record, UpsertRecordOptions? options = default, CancellationToken cancellationToken = default);
+}
+
+class AzureAISearchVectorRecordStore(): IVectorRecordStore;
+```
+
+#### Option 3 - Separated collection and record management with collection create separate from other operations.
+
+Vector store same as option 2 so not repeated for brevity.
+
+```cs
+
+interface IVectorCollectionCreate
+{
+ virtual Task CreateCollectionAsync(string name, CancellationToken cancellationToken = default);
+}
+
+// Implement a generic version of create that takes a configuration that should work for 80% of cases.
+class AzureAISearchConfiguredVectorCollectionCreate(CollectionCreateConfig collectionConfig): IVectorCollectionCreate;
+
+// Allow custom implementations of create for break glass scenarios for outside the 80% case.
+class AzureAISearchChatHistoryVectorCollectionCreate: IVectorCollectionCreate;
+class AzureAISearchSemanticCacheVectorCollectionCreate: IVectorCollectionCreate;
+
+// Customers can create their own creation scenarios to match their schemas, but can continue to use our get, does exist and delete class.
+class CustomerChatHistoryVectorCollectionCreate: IVectorCollectionCreate;
+
+interface IVectorCollectionNonSchema
+{
+ IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default);
+ Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default);
+ Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default);
+}
+
+class AzureAISearchVectorCollectionNonSchema: IVectorCollectionNonSchema;
+class RedisVectorCollectionNonSchema: IVectorCollectionNonSchema;
+class WeaviateVectorCollectionNonSchema: IVectorCollectionNonSchema;
+
+```
+
+#### Option 4 - Separated collection and record management with collection create separate from other operations, with collection management aggregation class on top.
+
+Variation on option 3.
+
+```cs
+
+interface IVectorCollectionCreate
+{
+ virtual Task CreateCollectionAsync(string name, CancellationToken cancellationToken = default);
+}
+
+interface IVectorCollectionNonSchema
+{
+ IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default);
+ Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default);
+ Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default);
+}
+
+// DB Specific NonSchema implementations
+class AzureAISearchVectorCollectionNonSchema: IVectorCollectionNonSchema;
+class RedisVectorCollectionNonSchema: IVectorCollectionNonSchema;
+
+// Combined Create + NonSchema Interface
+interface IVectorCollectionStore: IVectorCollectionCreate, IVectorCollectionNonSchema {}
+
+// Base abstract class that forwards non-create operations to provided implementation.
+abstract class VectorCollectionStore(IVectorCollectionNonSchema collectionNonSchema): IVectorCollectionStore
+{
+ public abstract Task CreateCollectionAsync(string name, CancellationToken cancellationToken = default);
+ public IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default) { return collectionNonSchema.ListCollectionNamesAsync(cancellationToken); }
+ public Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default) { return collectionNonSchema.CollectionExistsAsync(name, cancellationToken); }
+ public Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default) { return collectionNonSchema.DeleteCollectionAsync(name, cancellationToken); }
+}
+
+// Collections store implementations, that inherit from base class, and just adds the different creation implementations.
+class AzureAISearchChatHistoryVectorCollectionStore(AzureAISearchVectorCollectionNonSchema nonSchema): VectorCollectionStore(nonSchema);
+class AzureAISearchSemanticCacheVectorCollectionStore(AzureAISearchVectorCollectionNonSchema nonSchema): VectorCollectionStore(nonSchema);
+class AzureAISearchMLIndexVectorCollectionStore(AzureAISearchVectorCollectionNonSchema nonSchema): VectorCollectionStore(nonSchema);
+
+// Customer collections store implementation, that uses the base Azure AI Search implementation for get, doesExist and delete, but adds its own creation.
+class ContosoProductsVectorCollectionStore(AzureAISearchVectorCollectionNonSchema nonSchema): VectorCollectionStore(nonSchema);
+
+```
+
+#### Option 5 - Separated collection and record management with collection create separate from other operations, with overall aggregation class on top.
+
+Same as option 3 / 4, plus:
+
+```cs
+
+interface IVectorStore : IVectorCollectionStore, IVectorRecordStore
+{
+}
+
+// Create a static factory that produces one of these, so only the interface is public, not the class.
+internal class VectorStore(IVectorCollectionCreate create, IVectorCollectionNonSchema nonSchema, IVectorRecordStore records): IVectorStore
+{
+}
+
+```
+
+#### Option 6 - Collection store acts as factory for record store.
+
+`IVectorStore` acts as a factory for `IVectorStoreCollection`, and any schema agnostic multi-collection operations are kept on `IVectorStore`.
+
+
+```cs
+public interface IVectorStore
+{
+ IVectorStoreCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null);
+ IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default));
+}
+
+public interface IVectorStoreCollection
+{
+ public string Name { get; }
+
+ // Collection Operations
+ Task CreateCollectionAsync();
+ Task CreateCollectionIfNotExistsAsync();
+ Task CollectionExistsAsync();
+ Task DeleteCollectionAsync();
+
+ // Data manipulation
+ Task GetAsync(TKey key, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
+ IAsyncEnumerable GetBatchAsync(IEnumerable keys, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
+ Task DeleteAsync(TKey key, DeleteRecordOptions? options = default, CancellationToken cancellationToken = default);
+ Task DeleteBatchAsync(IEnumerable keys, DeleteRecordOptions? options = default, CancellationToken cancellationToken = default);
+ Task UpsertAsync(TRecord record, UpsertRecordOptions? options = default, CancellationToken cancellationToken = default);
+ IAsyncEnumerable UpsertBatchAsync(IEnumerable records, UpsertRecordOptions? options = default, CancellationToken cancellationToken = default);
+}
+```
+
+
+#### Decision Outcome
+
+Option 1 is problematic on its own, since we have to allow consumers to create custom implementations of collection create for break glass scenarios. With
+a single interface like this, it will require them to implement many methods that they do not want to change. Options 4 & 5, gives us more flexibility while
+still preserving the ease of use of an aggregated interface as described in Option 1.
+
+Option 2 doesn't give us the flexbility we need for break glass scenarios, since it only allows certain types of collections to be created. It also means
+that each time a new collection type is required it introduces a breaking change, so it is not a viable option.
+
+Since collection create and configuration and the possible options vary considerable across different database types, we will need to support an easy
+to use break glass scenario for collection creation. While we would be able to develop a basic configurable create option, for complex create scenarios
+users will need to implement their own. We will also need to support multiple create implementations out of the box, e.g. a configuration based option using
+our own configuration, create implementations that re-create the current model for backward compatibility, create implementations that use other configuration
+as input, e.g. Azure-ML YAML. Therefore separating create, which may have many implementations, from exists, list and delete, which requires only a single implementation per database type is useful.
+Option 3 provides us this separation, but Option 4 + 5 builds on top of this, and allows us to combine different implementations together for simpler
+consumption.
+
+Chosen option: 6
+
+- Easy to use, and similar to many SDk implementations.
+- Can pass a single object around for both collection and record access.
+
+### Question 2: Collection name and key value normalization in store, decorator or via injection.
+
+#### Option 1 - Normalization in main record store
+
+- Pros: Simple
+- Cons: The normalization needs to vary separately from the record store, so this will not work
+
+```cs
+ public class AzureAISearchVectorStoreCollection : IVectorStoreCollection
+ {
+ ...
+
+ // On input.
+ var normalizedCollectionName = this.NormalizeCollectionName(collectionName);
+ var encodedId = AzureAISearchMemoryRecord.EncodeId(key);
+
+ ...
+
+ // On output.
+ DecodeId(this.Id)
+
+ ...
+ }
+```
+
+#### Option 2 - Normalization in decorator
+
+- Pros: Allows normalization to vary separately from the record store.
+- Pros: No code executed when no normalization required.
+- Pros: Easy to package matching encoders/decoders together.
+- Pros: Easier to obsolete encoding/normalization as a concept.
+- Cons: Not a major con, but need to implement the full VectorStoreCollection interface, instead of e.g. just providing the two translation functions, if we go with option 3.
+- Cons: Hard to have a generic implementation that can work with any model, without either changing the data in the provided object on upsert or doing cloning in an expensive way.
+
+```cs
+ new KeyNormalizingAISearchVectorStoreCollection(
+ "keyField",
+ new AzureAISearchVectorStoreCollection(...));
+```
+
+#### Option 3 - Normalization via optional function parameters to record store constructor
+
+- Pros: Allows normalization to vary separately from the record store.
+- Pros: No need to implement the full VectorStoreCollection interface.
+- Pros: Can modify values on serialization without changing the incoming record, if supported by DB SDK.
+- Cons: Harder to package matching encoders/decoders together.
+
+```cs
+public class AzureAISearchVectorStoreCollection(StoreOptions options);
+
+public class StoreOptions
+{
+ public Func? EncodeKey { get; init; }
+ public Func? DecodeKey { get; init; }
+ public Func? SanitizeCollectionName { get; init; }
+}
+```
+
+#### Option 4 - Normalization via custom mapper
+
+If developer wants to change any values they can do so by creating a custom mapper.
+
+- Cons: Developer needs to implement a mapper if they want to do normalization.
+- Cons: Developer cannot change collection name as part of the mapping.
+- Pros: No new extension points required to support normalization.
+- Pros: Developer can change any field in the record.
+
+#### Decision Outcome
+
+Chosen option 3, since it is similar to how we are doing mapper injection and would also work well in python.
+
+Option 1 won't work because if e.g. the data was written using another tool, it may be unlikely that it was encoded using the same mechanism as supported here
+and therefore this functionality may not be appropriate. The developer should have the ability to not use this functionality or
+provide their own encoding / decoding behavior.
+
+### Question 3: Collection name as method param or via constructor or either
+
+#### Option 1 - Collection name as method param
+
+```cs
+public class MyVectorStoreCollection()
+{
+ public async Task GetAsync(string collectionName, string key, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
+}
+```
+
+#### Option 2 - Collection name via constructor
+
+```cs
+public class MyVectorStoreCollection(string defaultCollectionName)
+{
+ public async Task GetAsync(string key, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
+}
+```
+
+#### Option 3 - Collection name via either
+
+```cs
+public class MyVectorStoreCollection(string defaultCollectionName)
+{
+ public async Task GetAsync(string key, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
+}
+
+public class GetRecordOptions
+{
+ public string CollectionName { get; init; };
+}
+```
+
+#### Decision Outcome
+
+Chosen option 2. None of the other options work with the decision outcome of Question 1, since that design requires the `VectorStoreCollection` to be tied to a single collection instance.
+
+### Question 4: How to normalize ids across different vector stores where different types are supported.
+
+#### Option 1 - Take a string and convert to a type that was specified on the constructor
+
+```cs
+public async Task GetAsync(string key, GetRecordOptions? options = default, CancellationToken cancellationToken = default)
+{
+ var convertedKey = this.keyType switch
+ {
+ KeyType.Int => int.parse(key),
+ KeyType.GUID => Guid.parse(key)
+ }
+
+ ...
+}
+```
+
+- No additional overloads are required over time so no breaking changes.
+- Most data types can easily be represented in string form and converted to/from it.
+
+#### Option 2 - Take an object and cast to a type that was specified on the constructor.
+
+```cs
+public async Task GetAsync(object key, GetRecordOptions? options = default, CancellationToken cancellationToken = default)
+{
+ var convertedKey = this.keyType switch
+ {
+ KeyType.Int => key as int,
+ KeyType.GUID => key as Guid
+ }
+
+ if (convertedKey is null)
+ {
+ throw new InvalidOperationException($"The provided key must be of type {this.keyType}")
+ }
+
+ ...
+}
+
+```
+
+- No additional overloads are required over time so no breaking changes.
+- Any data types can be represented as object.
+
+#### Option 3 - Multiple overloads where we convert where possible, throw when not possible.
+
+```cs
+public async Task GetAsync(string key, GetRecordOptions? options = default, CancellationToken cancellationToken = default)
+{
+ var convertedKey = this.keyType switch
+ {
+ KeyType.Int => int.Parse(key),
+ KeyType.String => key,
+ KeyType.GUID => Guid.Parse(key)
+ }
+}
+public async Task GetAsync(int key, GetRecordOptions? options = default, CancellationToken cancellationToken = default)
+{
+ var convertedKey = this.keyType switch
+ {
+ KeyType.Int => key,
+ KeyType.String => key.ToString(),
+ KeyType.GUID => throw new InvalidOperationException($"The provided key must be convertible to a GUID.")
+ }
+}
+public async Task GetAsync(GUID key, GetRecordOptions? options = default, CancellationToken cancellationToken = default)
+{
+ var convertedKey = this.keyType switch
+ {
+ KeyType.Int => throw new InvalidOperationException($"The provided key must be convertible to an int.")
+ KeyType.String => key.ToString(),
+ KeyType.GUID => key
+ }
+}
+```
+
+- Additional overloads are required over time if new key types are found on new connectors, causing breaking changes.
+- You can still call a method that causes a runtime error, when the type isn't supported.
+
+#### Option 4 - Add key type as generic to interface
+
+```cs
+interface IVectorRecordStore
+{
+ Task GetAsync(TKey key, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
+}
+
+class AzureAISearchVectorRecordStore: IVectorRecordStore
+{
+ public AzureAISearchVectorRecordStore()
+ {
+ // Check if TKey matches the type of the field marked as a key on TRecord and throw if they don't match.
+ // Also check if keytype is one of the allowed types for Azure AI Search and throw if it isn't.
+ }
+}
+
+```
+
+- No runtime issues after construction.
+- More cumbersome interface.
+
+#### Decision Outcome
+
+Chosen option 4, since it is forwards compatible with any complex key types we may need to support but still allows
+each implementation to hardcode allowed key types if the vector db only supports certain key types.
+
+### Question 5: Store Interface/Class Naming.
+
+#### Option 1 - VectorDB
+
+```cs
+interface IVectorDBRecordService {}
+interface IVectorDBCollectionUpdateService {}
+interface IVectorDBCollectionCreateService {}
+```
+
+#### Option 2 - Memory
+
+```cs
+interface IMemoryRecordService {}
+interface IMemoryCollectionUpdateService {}
+interface IMemoryCollectionCreateService {}
+```
+
+### Option 3 - VectorStore
+
+```cs
+interface IVectorRecordStore {}
+interface IVectorCollectionNonSchema {}
+interface IVectorCollectionCreate {}
+interface IVectorCollectionStore {}: IVectorCollectionCreate, IVectorCollectionNonSchema
+interface IVectorStore {}: IVectorCollectionStore, IVectorRecordStore
+```
+
+### Option 4 - VectorStore + VectorStoreCollection
+
+```cs
+interface IVectorStore
+{
+ IVectorStoreCollection GetCollection()
+}
+interface IVectorStoreCollection
+{
+ Get()
+ Delete()
+ Upsert()
+}
+```
+
+#### Decision Outcome
+
+Chosen option 4. The word memory is broad enough to encompass any data, so using it seems arbitrary. All competitors are using the term vector store, so using something similar is good for recognition.
+Option 4 also matches our design as chosen in question 1.
+
+## Usage Examples
+
+### DI Framework: .net 8 Keyed Services
+
+```cs
+class CacheEntryModel(string prompt, string result, ReadOnlyMemory promptEmbedding);
+
+class SemanticTextMemory(IVectorStore configuredVectorStore, VectorStoreRecordDefinition? vectorStoreRecordDefinition): ISemanticTextMemory
+{
+ public async Task SaveInformation(string collectionName, TDataType record)
+ {
+ var collection = vectorStore.GetCollection(collectionName, vectorStoreRecordDefinition);
+ if (!await collection.CollectionExists())
+ {
+ await collection.CreateCollection();
+ }
+ await collection.UpsertAsync(record);
+ }
+}
+
+class CacheSetFunctionFilter(ISemanticTextMemory memory); // Saves results to cache.
+class CacheGetPromptFilter(ISemanticTextMemory memory); // Check cache for entries.
+
+var builder = Kernel.CreateBuilder();
+
+builder
+ // Existing registration:
+ .AddAzureOpenAITextEmbeddingGeneration(textEmbeddingDeploymentName, azureAIEndpoint, apiKey, serviceId: "AzureOpenAI:text-embedding-ada-002")
+
+ // Register an IVectorStore implementation under the given key.
+ .AddAzureAISearch("Cache", azureAISearchEndpoint, apiKey, new Options() { withEmbeddingGeneration = true });
+
+// Add Semantic Cache Memory for the cache entry model.
+builder.Services.AddTransient(sp => {
+ return new SemanticTextMemory(
+ sp.GetKeyedService("Cache"),
+ cacheRecordDefinition);
+});
+
+// Add filter to retrieve items from cache and one to add items to cache.
+// Since these filters depend on ISemanticTextMemory and that is already registered, it should get matched automatically.
+builder.Services.AddTransient();
+builder.Services.AddTransient();
+```
+
+## Roadmap
+
+### Record Management
+
+1. Release VectorStoreCollection public interface and implementations for Azure AI Search, Qdrant and Redis.
+2. Add support for registering record stores with SK container to allow automatic dependency injection.
+3. Add VectorStoreCollection implementations for remaining stores.
+
+### Collection Management
+
+4. Release Collection Management public interface and implementations for Azure AI Search, Qdrant and Redis.
+5. Add support for registering collection management with SK container to allow automatic dependency injection.
+6. Add Collection Management implementations for remaining stores.
+
+### Collection Creation
+
+7. Release Collection Creation public interface.
+8. Create cross db collection creation config that supports common functionality, and per daatabase implementation that supports this configuration.
+9. Add support for registering collection creation with SK container to allow automatic dependency injection.
+
+### First Party Memory Features and well known model support
+
+10. Add model and mappers for legacy SK MemoryStore interface, so that consumers using this has an upgrade path to the new memory storage stack.
+11. Add model and mappers for popular loader systems, like Kernel Memory or LlamaIndex.
+11. Explore adding first party implementations for common scenarios, e.g. semantic caching. Specfics TBD.
+
+### Cross Cutting Requirements
+
+Need the following for all features:
+
+- Unit tests
+- Integration tests
+- Logging / Telemetry
+- Common Exception Handling
+- Samples, including:
+ - Usage scenario for collection and record management using custom model and configured collection creation.
+ - A simple consumption example like semantic caching, specfics TBD.
+ - Adding your own collection creation implementation.
+ - Adding your own custom model mapper.
+- Documentation, including:
+ - How to create models and annotate/describe them to use with the storage system.
+ - How to define configuration for creating collections using common create implementation.
+ - How to use record and collection management apis.
+ - How to implement your own collection create implementation for break glass scenario.
+ - How to implement your own mapper.
+ - How to upgrade from the current storage system to the new one.
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 645c8a249d2a..b1c7dc58eddc 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -10,7 +10,7 @@
-
+
@@ -18,7 +18,7 @@
-
+
@@ -27,9 +27,10 @@
-
+
+
@@ -38,12 +39,12 @@
-
+
-
+
@@ -68,23 +69,24 @@
+
-
+
-
+
-
-
+
+
-
+
@@ -92,6 +94,7 @@
+
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index 6574700e6ce6..b6cd87d2040b 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -318,7 +318,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Qdrant.UnitTests", "src\Connectors\Connectors.Qdrant.UnitTests\Connectors.Qdrant.UnitTests.csproj", "{E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -795,6 +797,12 @@ Global
{38374C62-0263-4FE8-A18C-70FC8132912B}.Publish|Any CPU.Build.0 = Debug|Any CPU
{38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
+ {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Publish|Any CPU.Build.0 = Debug|Any CPU
+ {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -904,6 +912,7 @@ Global
{1D4667B9-9381-4E32-895F-123B94253EE8} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{38374C62-0263-4FE8-A18C-70FC8132912B} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
+ {E06818E3-00A5-41AC-97ED-9491070CDEA1} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index 3e173111cdd3..00837d71f910 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -1,7 +1,7 @@
- 1.16.1
+ 1.17.1$(VersionPrefix)-$(VersionSuffix)$(VersionPrefix)
@@ -10,7 +10,7 @@
true
- 1.16.1
+ 1.17.0$(NoWarn);CP0003
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
index f344dae432b9..16c019aebbfd 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
@@ -21,8 +22,8 @@ public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync()
new()
{
Instructions = "Answer questions about the menu.",
- Kernel = CreateKernelWithChatCompletion(),
- ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions },
+ Kernel = CreateKernelWithFilter(),
+ Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }),
};
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
@@ -74,8 +75,8 @@ public async Task UseAutoFunctionInvocationFilterWithAgentChatAsync()
new()
{
Instructions = "Answer questions about the menu.",
- Kernel = CreateKernelWithChatCompletion(),
- ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions },
+ Kernel = CreateKernelWithFilter(),
+ Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }),
};
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
@@ -119,17 +120,41 @@ private void WriteContent(ChatMessageContent content)
Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'");
}
+ private Kernel CreateKernelWithFilter()
+ {
+ IKernelBuilder builder = Kernel.CreateBuilder();
+
+ if (this.UseOpenAIConfig)
+ {
+ builder.AddOpenAIChatCompletion(
+ TestConfiguration.OpenAI.ChatModelId,
+ TestConfiguration.OpenAI.ApiKey);
+ }
+ else
+ {
+ builder.AddAzureOpenAIChatCompletion(
+ TestConfiguration.AzureOpenAI.ChatDeploymentName,
+ TestConfiguration.AzureOpenAI.Endpoint,
+ TestConfiguration.AzureOpenAI.ApiKey);
+ }
+
+ builder.Services.AddSingleton(new AutoInvocationFilter());
+
+ return builder.Build();
+ }
+
private sealed class MenuPlugin
{
[KernelFunction, Description("Provides a list of specials from the menu.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")]
public string GetSpecials()
{
- return @"
-Special Soup: Clam Chowder
-Special Salad: Cobb Salad
-Special Drink: Chai Tea
-";
+ return
+ """
+ Special Soup: Clam Chowder
+ Special Salad: Cobb Salad
+ Special Drink: Chai Tea
+ """;
}
[KernelFunction, Description("Provides the price of the requested menu item.")]
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_HistoryReducer.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_HistoryReducer.cs
new file mode 100644
index 000000000000..6e0816bc8470
--- /dev/null
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_HistoryReducer.cs
@@ -0,0 +1,173 @@
+// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.History;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace Agents;
+
+///
+/// Demonstrate creation of and
+/// eliciting its response to three explicit user messages.
+///
+public class ChatCompletion_HistoryReducer(ITestOutputHelper output) : BaseTest(output)
+{
+ private const string TranslatorName = "NumeroTranslator";
+ private const string TranslatorInstructions = "Add one to latest user number and spell it in spanish without explanation.";
+
+ ///
+ /// Demonstrate the use of when directly
+ /// invoking a .
+ ///
+ [Fact]
+ public async Task TruncatedAgentReductionAsync()
+ {
+ // Define the agent
+ ChatCompletionAgent agent = CreateTruncatingAgent(10, 10);
+
+ await InvokeAgentAsync(agent, 50);
+ }
+
+ ///
+ /// Demonstrate the use of when directly
+ /// invoking a .
+ ///
+ [Fact]
+ public async Task SummarizedAgentReductionAsync()
+ {
+ // Define the agent
+ ChatCompletionAgent agent = CreateSummarizingAgent(10, 10);
+
+ await InvokeAgentAsync(agent, 50);
+ }
+
+ ///
+ /// Demonstrate the use of when using
+ /// to invoke a .
+ ///
+ [Fact]
+ public async Task TruncatedChatReductionAsync()
+ {
+ // Define the agent
+ ChatCompletionAgent agent = CreateTruncatingAgent(10, 10);
+
+ await InvokeChatAsync(agent, 50);
+ }
+
+ ///
+ /// Demonstrate the use of when using
+ /// to invoke a .
+ ///
+ [Fact]
+ public async Task SummarizedChatReductionAsync()
+ {
+ // Define the agent
+ ChatCompletionAgent agent = CreateSummarizingAgent(10, 10);
+
+ await InvokeChatAsync(agent, 50);
+ }
+
+ // Proceed with dialog by directly invoking the agent and explicitly managing the history.
+ private async Task InvokeAgentAsync(ChatCompletionAgent agent, int messageCount)
+ {
+ ChatHistory chat = [];
+
+ int index = 1;
+ while (index <= messageCount)
+ {
+ // Provide user input
+ chat.Add(new ChatMessageContent(AuthorRole.User, $"{index}"));
+ Console.WriteLine($"# {AuthorRole.User}: '{index}'");
+
+ // Reduce prior to invoking the agent
+ bool isReduced = await agent.ReduceAsync(chat);
+
+ // Invoke and display assistant response
+ await foreach (ChatMessageContent message in agent.InvokeAsync(chat))
+ {
+ chat.Add(message);
+ Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'");
+ }
+
+ index += 2;
+
+ // Display the message count of the chat-history for visibility into reduction
+ Console.WriteLine($"@ Message Count: {chat.Count}\n");
+
+ // Display summary messages (if present) if reduction has occurred
+ if (isReduced)
+ {
+ int summaryIndex = 0;
+ while (chat[summaryIndex].Metadata?.ContainsKey(ChatHistorySummarizationReducer.SummaryMetadataKey) ?? false)
+ {
+ Console.WriteLine($"\tSummary: {chat[summaryIndex].Content}");
+ ++summaryIndex;
+ }
+ }
+ }
+ }
+
+ // Proceed with dialog with AgentGroupChat.
+ private async Task InvokeChatAsync(ChatCompletionAgent agent, int messageCount)
+ {
+ AgentGroupChat chat = new();
+
+ int lastHistoryCount = 0;
+
+ int index = 1;
+ while (index <= messageCount)
+ {
+ // Provide user input
+ chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, $"{index}"));
+ Console.WriteLine($"# {AuthorRole.User}: '{index}'");
+
+ // Invoke and display assistant response
+ await foreach (ChatMessageContent message in chat.InvokeAsync(agent))
+ {
+ Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'");
+ }
+
+ index += 2;
+
+ // Display the message count of the chat-history for visibility into reduction
+ // Note: Messages provided in descending order (newest first)
+ ChatMessageContent[] history = await chat.GetChatMessagesAsync(agent).ToArrayAsync();
+ Console.WriteLine($"@ Message Count: {history.Length}\n");
+
+ // Display summary messages (if present) if reduction has occurred
+ if (history.Length < lastHistoryCount)
+ {
+ int summaryIndex = history.Length - 1;
+ while (history[summaryIndex].Metadata?.ContainsKey(ChatHistorySummarizationReducer.SummaryMetadataKey) ?? false)
+ {
+ Console.WriteLine($"\tSummary: {history[summaryIndex].Content}");
+ --summaryIndex;
+ }
+ }
+
+ lastHistoryCount = history.Length;
+ }
+ }
+
+ private ChatCompletionAgent CreateSummarizingAgent(int reducerMessageCount, int reducerThresholdCount)
+ {
+ Kernel kernel = this.CreateKernelWithChatCompletion();
+ return
+ new()
+ {
+ Name = TranslatorName,
+ Instructions = TranslatorInstructions,
+ Kernel = kernel,
+ HistoryReducer = new ChatHistorySummarizationReducer(kernel.GetRequiredService(), reducerMessageCount, reducerThresholdCount),
+ };
+ }
+
+ private ChatCompletionAgent CreateTruncatingAgent(int reducerMessageCount, int reducerThresholdCount) =>
+ new()
+ {
+ Name = TranslatorName,
+ Instructions = TranslatorInstructions,
+ Kernel = this.CreateKernelWithChatCompletion(),
+ HistoryReducer = new ChatHistoryTruncationReducer(reducerMessageCount, reducerThresholdCount),
+ };
+}
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs
new file mode 100644
index 000000000000..82b2ca28bce0
--- /dev/null
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs
@@ -0,0 +1,128 @@
+// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+
+namespace Agents;
+
+///
+/// Demonstrate service selection for through setting service-id
+/// on and also providing override
+/// when calling
+///
+public class ChatCompletion_ServiceSelection(ITestOutputHelper output) : BaseTest(output)
+{
+ private const string ServiceKeyGood = "chat-good";
+ private const string ServiceKeyBad = "chat-bad";
+
+ [Fact]
+ public async Task UseServiceSelectionWithChatCompletionAgentAsync()
+ {
+ // Create kernel with two instances of IChatCompletionService
+ // One service is configured with a valid API key and the other with an
+ // invalid key that will result in a 401 Unauthorized error.
+ Kernel kernel = CreateKernelWithTwoServices();
+
+ // Define the agent targeting ServiceId = ServiceKeyGood
+ ChatCompletionAgent agentGood =
+ new()
+ {
+ Kernel = kernel,
+ Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyGood }),
+ };
+
+ // Define the agent targeting ServiceId = ServiceKeyBad
+ ChatCompletionAgent agentBad =
+ new()
+ {
+ Kernel = kernel,
+ Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyBad }),
+ };
+
+ // Define the agent with no explicit ServiceId defined
+ ChatCompletionAgent agentDefault = new() { Kernel = kernel };
+
+ // Invoke agent as initialized with ServiceId = ServiceKeyGood: Expect agent response
+ Console.WriteLine("\n[Agent With Good ServiceId]");
+ await InvokeAgentAsync(agentGood);
+
+ // Invoke agent as initialized with ServiceId = ServiceKeyBad: Expect failure due to invalid service key
+ Console.WriteLine("\n[Agent With Bad ServiceId]");
+ await InvokeAgentAsync(agentBad);
+
+ // Invoke agent as initialized with no explicit ServiceId: Expect agent response
+ Console.WriteLine("\n[Agent With No ServiceId]");
+ await InvokeAgentAsync(agentDefault);
+
+ // Invoke agent with override arguments where ServiceId = ServiceKeyGood: Expect agent response
+ Console.WriteLine("\n[Bad Agent: Good ServiceId Override]");
+ await InvokeAgentAsync(agentBad, new(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyGood }));
+
+ // Invoke agent with override arguments where ServiceId = ServiceKeyBad: Expect failure due to invalid service key
+ Console.WriteLine("\n[Good Agent: Bad ServiceId Override]");
+ await InvokeAgentAsync(agentGood, new(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyBad }));
+ Console.WriteLine("\n[Default Agent: Bad ServiceId Override]");
+ await InvokeAgentAsync(agentDefault, new(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyBad }));
+
+ // Invoke agent with override arguments with no explicit ServiceId: Expect agent response
+ Console.WriteLine("\n[Good Agent: No ServiceId Override]");
+ await InvokeAgentAsync(agentGood, new(new OpenAIPromptExecutionSettings()));
+ Console.WriteLine("\n[Bad Agent: No ServiceId Override]");
+ await InvokeAgentAsync(agentBad, new(new OpenAIPromptExecutionSettings()));
+ Console.WriteLine("\n[Default Agent: No ServiceId Override]");
+ await InvokeAgentAsync(agentDefault, new(new OpenAIPromptExecutionSettings()));
+
+ // Local function to invoke agent and display the conversation messages.
+ async Task InvokeAgentAsync(ChatCompletionAgent agent, KernelArguments? arguments = null)
+ {
+ ChatHistory chat = [new(AuthorRole.User, "Hello")];
+
+ try
+ {
+ await foreach (ChatMessageContent response in agent.InvokeAsync(chat, arguments))
+ {
+ Console.WriteLine(response.Content);
+ }
+ }
+ catch (HttpOperationException exception)
+ {
+ Console.WriteLine($"Status: {exception.StatusCode}");
+ }
+ }
+ }
+
+ private Kernel CreateKernelWithTwoServices()
+ {
+ IKernelBuilder builder = Kernel.CreateBuilder();
+
+ if (this.UseOpenAIConfig)
+ {
+ builder.AddOpenAIChatCompletion(
+ TestConfiguration.OpenAI.ChatModelId,
+ "bad-key",
+ serviceId: ServiceKeyBad);
+
+ builder.AddOpenAIChatCompletion(
+ TestConfiguration.OpenAI.ChatModelId,
+ TestConfiguration.OpenAI.ApiKey,
+ serviceId: ServiceKeyGood);
+ }
+ else
+ {
+ builder.AddAzureOpenAIChatCompletion(
+ TestConfiguration.AzureOpenAI.ChatDeploymentName,
+ TestConfiguration.AzureOpenAI.Endpoint,
+ "bad-key",
+ serviceId: ServiceKeyBad);
+
+ builder.AddAzureOpenAIChatCompletion(
+ TestConfiguration.AzureOpenAI.ChatDeploymentName,
+ TestConfiguration.AzureOpenAI.Endpoint,
+ TestConfiguration.AzureOpenAI.ApiKey,
+ serviceId: ServiceKeyGood);
+ }
+
+ return builder.Build();
+ }
+}
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
index 258e12166a6b..d3e94386af96 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
@@ -49,7 +49,7 @@ public async Task UseStreamingChatCompletionAgentWithPluginAsync()
Name = "Host",
Instructions = MenuInstructions,
Kernel = this.CreateKernelWithChatCompletion(),
- ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions },
+ Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }),
};
// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage).
diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs b/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs
new file mode 100644
index 000000000000..92aa8a9ce9d4
--- /dev/null
+++ b/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+
+namespace Agents;
+
+///
+/// Demonstrate the use of .
+///
+public class MixedChat_Reset(ITestOutputHelper output) : BaseTest(output)
+{
+ private const string AgentInstructions =
+ """
+ The user may either provide information or query on information previously provided.
+ If the query does not correspond with information provided, inform the user that their query cannot be answered.
+ """;
+
+ [Fact]
+ public async Task ResetChatAsync()
+ {
+ OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey);
+
+ // Define the agents
+ OpenAIAssistantAgent assistantAgent =
+ await OpenAIAssistantAgent.CreateAsync(
+ kernel: new(),
+ config: new(this.ApiKey, this.Endpoint),
+ new()
+ {
+ Name = nameof(OpenAIAssistantAgent),
+ Instructions = AgentInstructions,
+ ModelId = this.Model,
+ });
+
+ ChatCompletionAgent chatAgent =
+ new()
+ {
+ Name = nameof(ChatCompletionAgent),
+ Instructions = AgentInstructions,
+ Kernel = this.CreateKernelWithChatCompletion(),
+ };
+
+ // Create a chat for agent interaction.
+ AgentGroupChat chat = new();
+
+ // Respond to user input
+ try
+ {
+ await InvokeAgentAsync(assistantAgent, "What is my favorite color?");
+ await InvokeAgentAsync(chatAgent);
+
+ await InvokeAgentAsync(assistantAgent, "I like green.");
+ await InvokeAgentAsync(chatAgent);
+
+ await InvokeAgentAsync(assistantAgent, "What is my favorite color?");
+ await InvokeAgentAsync(chatAgent);
+
+ await chat.ResetAsync();
+
+ await InvokeAgentAsync(assistantAgent, "What is my favorite color?");
+ await InvokeAgentAsync(chatAgent);
+ }
+ finally
+ {
+ await chat.ResetAsync();
+ await assistantAgent.DeleteAsync();
+ }
+
+ // Local function to invoke agent and display the conversation messages.
+ async Task InvokeAgentAsync(Agent agent, string? input = null)
+ {
+ if (!string.IsNullOrWhiteSpace(input))
+ {
+ chat.AddChatMessage(new(AuthorRole.User, input));
+ Console.WriteLine($"\n# {AuthorRole.User}: '{input}'");
+ }
+
+ await foreach (ChatMessageContent message in chat.InvokeAsync(agent))
+ {
+ if (!string.IsNullOrWhiteSpace(message.Content))
+ {
+ Console.WriteLine($"\n# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'");
+ }
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs b/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs
index 22b6eec9baaf..46aadfc243b0 100644
--- a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs
+++ b/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using Azure.Identity;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
@@ -11,7 +12,7 @@ public class OpenAI_ChatCompletion(ITestOutputHelper output) : BaseTest(output)
[Fact]
public async Task OpenAIChatSampleAsync()
{
- Console.WriteLine("======== Open AI - ChatGPT ========");
+ Console.WriteLine("======== Open AI - Chat Completion ========");
OpenAIChatCompletionService chatCompletionService = new(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey);
@@ -49,7 +50,7 @@ I hope these suggestions are helpful!
[Fact]
public async Task AzureOpenAIChatSampleAsync()
{
- Console.WriteLine("======== Azure Open AI - ChatGPT ========");
+ Console.WriteLine("======== Azure Open AI - Chat Completion ========");
AzureOpenAIChatCompletionService chatCompletionService = new(
deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName,
@@ -60,6 +61,24 @@ public async Task AzureOpenAIChatSampleAsync()
await StartChatAsync(chatCompletionService);
}
+ ///
+ /// Sample showing how to use Azure Open AI Chat Completion with Azure Default Credential.
+ /// If local auth is disabled in the Azure Open AI deployment, you can use Azure Default Credential to authenticate.
+ ///
+ [Fact]
+ public async Task AzureOpenAIWithDefaultAzureCredentialSampleAsync()
+ {
+ Console.WriteLine("======== Azure Open AI - Chat Completion with Azure Default Credential ========");
+
+ AzureOpenAIChatCompletionService chatCompletionService = new(
+ deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName,
+ endpoint: TestConfiguration.AzureOpenAI.Endpoint,
+ credentials: new DefaultAzureCredential(),
+ modelId: TestConfiguration.AzureOpenAI.ChatModelId);
+
+ await StartChatAsync(chatCompletionService);
+ }
+
private async Task StartChatAsync(IChatCompletionService chatGPT)
{
Console.WriteLine("Chat content:");
diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj
index dd43184b6612..89cc2c897d61 100644
--- a/dotnet/samples/Concepts/Concepts.csproj
+++ b/dotnet/samples/Concepts/Concepts.csproj
@@ -14,6 +14,7 @@
+
diff --git a/dotnet/samples/Concepts/Memory/TextChunkerUsage.cs b/dotnet/samples/Concepts/Memory/TextChunkerUsage.cs
index a42e769ae916..51d50c619903 100644
--- a/dotnet/samples/Concepts/Memory/TextChunkerUsage.cs
+++ b/dotnet/samples/Concepts/Memory/TextChunkerUsage.cs
@@ -8,7 +8,7 @@ namespace Memory;
public class TextChunkerUsage(ITestOutputHelper output) : BaseTest(output)
{
- private static readonly Tokenizer s_tokenizer = Tokenizer.CreateTiktokenForModel("gpt-4");
+ private static readonly Tokenizer s_tokenizer = TiktokenTokenizer.CreateForModel("gpt-4");
[Fact]
public void RunExample()
diff --git a/dotnet/samples/Concepts/Memory/TextChunkingAndEmbedding.cs b/dotnet/samples/Concepts/Memory/TextChunkingAndEmbedding.cs
index 013bb4961621..04a74656e948 100644
--- a/dotnet/samples/Concepts/Memory/TextChunkingAndEmbedding.cs
+++ b/dotnet/samples/Concepts/Memory/TextChunkingAndEmbedding.cs
@@ -9,7 +9,7 @@ namespace Memory;
public class TextChunkingAndEmbedding(ITestOutputHelper output) : BaseTest(output)
{
private const string EmbeddingModelName = "text-embedding-ada-002";
- private static readonly Tokenizer s_tokenizer = Tokenizer.CreateTiktokenForModel(EmbeddingModelName);
+ private static readonly Tokenizer s_tokenizer = TiktokenTokenizer.CreateForModel(EmbeddingModelName);
[Fact]
public async Task RunAsync()
diff --git a/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreInfra.cs b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreInfra.cs
new file mode 100644
index 000000000000..ea498f20c5ab
--- /dev/null
+++ b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreInfra.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Docker.DotNet;
+using Docker.DotNet.Models;
+
+namespace Memory.VectorStoreFixtures;
+
+///
+/// Helper class that creates and deletes containers for the vector store examples.
+///
+internal static class VectorStoreInfra
+{
+ ///
+ /// Setup the qdrant container by pulling the image and running it.
+ ///
+ /// The docker client to create the container with.
+ /// The id of the container.
+ public static async Task SetupQdrantContainerAsync(DockerClient client)
+ {
+ await client.Images.CreateImageAsync(
+ new ImagesCreateParameters
+ {
+ FromImage = "qdrant/qdrant",
+ Tag = "latest",
+ },
+ null,
+ new Progress());
+
+ var container = await client.Containers.CreateContainerAsync(new CreateContainerParameters()
+ {
+ Image = "qdrant/qdrant",
+ HostConfig = new HostConfig()
+ {
+ PortBindings = new Dictionary>
+ {
+ {"6333", new List {new() {HostPort = "6333" } }},
+ {"6334", new List {new() {HostPort = "6334" } }}
+ },
+ PublishAllPorts = true
+ },
+ ExposedPorts = new Dictionary
+ {
+ { "6333", default },
+ { "6334", default }
+ },
+ });
+
+ await client.Containers.StartContainerAsync(
+ container.ID,
+ new ContainerStartParameters());
+
+ return container.ID;
+ }
+
+ ///
+ /// Setup the redis container by pulling the image and running it.
+ ///
+ /// The docker client to create the container with.
+ /// The id of the container.
+ public static async Task SetupRedisContainerAsync(DockerClient client)
+ {
+ await client.Images.CreateImageAsync(
+ new ImagesCreateParameters
+ {
+ FromImage = "redis/redis-stack",
+ Tag = "latest",
+ },
+ null,
+ new Progress());
+
+ var container = await client.Containers.CreateContainerAsync(new CreateContainerParameters()
+ {
+ Image = "redis/redis-stack",
+ HostConfig = new HostConfig()
+ {
+ PortBindings = new Dictionary>
+ {
+ {"6379", new List {new() {HostPort = "6379"}}},
+ {"8001", new List {new() {HostPort = "8001"}}}
+ },
+ PublishAllPorts = true
+ },
+ ExposedPorts = new Dictionary
+ {
+ { "6379", default },
+ { "8001", default }
+ },
+ });
+
+ await client.Containers.StartContainerAsync(
+ container.ID,
+ new ContainerStartParameters());
+
+ return container.ID;
+ }
+
+ ///
+ /// Stop and delete the container with the specified id.
+ ///
+ /// The docker client to delete the container in.
+ /// The id of the container to delete.
+ /// An async task.
+ public static async Task DeleteContainerAsync(DockerClient client, string containerId)
+ {
+ await client.Containers.StopContainerAsync(containerId, new ContainerStopParameters());
+ await client.Containers.RemoveContainerAsync(containerId, new ContainerRemoveParameters());
+ }
+}
diff --git a/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreQdrantContainerFixture.cs b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreQdrantContainerFixture.cs
new file mode 100644
index 000000000000..820b5d3bf172
--- /dev/null
+++ b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreQdrantContainerFixture.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Docker.DotNet;
+using Qdrant.Client;
+
+namespace Memory.VectorStoreFixtures;
+
+///
+/// Fixture to use for creating a Qdrant container before tests and delete it after tests.
+///
+public class VectorStoreQdrantContainerFixture : IAsyncLifetime
+{
+ private DockerClient? _dockerClient;
+ private string? _qdrantContainerId;
+
+ public async Task InitializeAsync()
+ {
+ }
+
+ public async Task ManualInitializeAsync()
+ {
+ if (this._qdrantContainerId == null)
+ {
+ // Connect to docker and start the docker container.
+ using var dockerClientConfiguration = new DockerClientConfiguration();
+ this._dockerClient = dockerClientConfiguration.CreateClient();
+ this._qdrantContainerId = await VectorStoreInfra.SetupQdrantContainerAsync(this._dockerClient);
+
+ // Delay until the Qdrant server is ready.
+ var qdrantClient = new QdrantClient("localhost");
+ var succeeded = false;
+ var attemptCount = 0;
+ while (!succeeded && attemptCount++ < 10)
+ {
+ try
+ {
+ await qdrantClient.ListCollectionsAsync();
+ succeeded = true;
+ }
+ catch (Exception)
+ {
+ await Task.Delay(1000);
+ }
+ }
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (this._dockerClient != null && this._qdrantContainerId != null)
+ {
+ // Delete docker container.
+ await VectorStoreInfra.DeleteContainerAsync(this._dockerClient, this._qdrantContainerId);
+ }
+ }
+}
diff --git a/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreRedisContainerFixture.cs b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreRedisContainerFixture.cs
new file mode 100644
index 000000000000..eb35b7ff555f
--- /dev/null
+++ b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreRedisContainerFixture.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Docker.DotNet;
+
+namespace Memory.VectorStoreFixtures;
+
+///
+/// Fixture to use for creating a Redis container before tests and delete it after tests.
+///
+public class VectorStoreRedisContainerFixture : IAsyncLifetime
+{
+ private DockerClient? _dockerClient;
+ private string? _redisContainerId;
+
+ public async Task InitializeAsync()
+ {
+ }
+
+ public async Task ManualInitializeAsync()
+ {
+ if (this._redisContainerId == null)
+ {
+ // Connect to docker and start the docker container.
+ using var dockerClientConfiguration = new DockerClientConfiguration();
+ this._dockerClient = dockerClientConfiguration.CreateClient();
+ this._redisContainerId = await VectorStoreInfra.SetupRedisContainerAsync(this._dockerClient);
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (this._dockerClient != null && this._redisContainerId != null)
+ {
+ // Delete docker container.
+ await VectorStoreInfra.DeleteContainerAsync(this._dockerClient, this._redisContainerId);
+ }
+ }
+}
diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs
new file mode 100644
index 000000000000..db8e259f4e7a
--- /dev/null
+++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs
@@ -0,0 +1,204 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Memory.VectorStoreFixtures;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Microsoft.SemanticKernel.Connectors.Redis;
+using Microsoft.SemanticKernel.Data;
+using Microsoft.SemanticKernel.Embeddings;
+using StackExchange.Redis;
+
+namespace Memory;
+
+///
+/// An example showing how to ingest data into a vector store using with a custom mapper.
+/// In this example, the storage model differs significantly from the data model, so a custom mapper is used to map between the two.
+/// A is used to define the schema of the storage model, and this means that the connector
+/// will not try and infer the schema from the data model.
+/// In storage the data is stored as a JSON object that looks similar to this:
+///
+/// {
+/// "Term": "API",
+/// "Definition": "Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data.",
+/// "DefinitionEmbedding": [ ... ]
+/// }
+///
+/// However, the data model is a class with a property for key and two dictionaries for the data (Term and Definition) and vector (DefinitionEmbedding).
+///
+/// The example shows the following steps:
+/// 1. Create an embedding generator.
+/// 2. Create a Redis Vector Store using a custom factory for creating collections.
+/// When constructing a collection, the factory injects a custom mapper that maps between the data model and the storage model if required.
+/// 3. Ingest some data into the vector store.
+/// 4. Read the data back from the vector store.
+///
+/// You need a local instance of Docker running, since the associated fixture will try and start a Redis container in the local docker instance to run against.
+///
+public class VectorStore_DataIngestion_CustomMapper(ITestOutputHelper output, VectorStoreRedisContainerFixture redisFixture) : BaseTest(output), IClassFixture
+{
+ ///
+ /// A record definition for the glossary entries that defines the storage schema of the record.
+ ///
+ private static readonly VectorStoreRecordDefinition s_glossaryDefinition = new()
+ {
+ Properties = new List
+ {
+ new VectorStoreRecordKeyProperty("Key", typeof(string)),
+ new VectorStoreRecordDataProperty("Term", typeof(string)),
+ new VectorStoreRecordDataProperty("Definition", typeof(string)),
+ new VectorStoreRecordVectorProperty("DefinitionEmbedding", typeof(ReadOnlyMemory)) { Dimensions = 1536, DistanceFunction = DistanceFunction.DotProductSimilarity }
+ }
+ };
+
+ [Fact]
+ public async Task ExampleAsync()
+ {
+ // Create an embedding generation service.
+ var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService(
+ TestConfiguration.AzureOpenAIEmbeddings.DeploymentName,
+ TestConfiguration.AzureOpenAIEmbeddings.Endpoint,
+ TestConfiguration.AzureOpenAIEmbeddings.ApiKey);
+
+ // Initiate the docker container and construct the vector store using the custom factory for creating collections.
+ await redisFixture.ManualInitializeAsync();
+ ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379");
+ var vectorStore = new RedisVectorStore(redis.GetDatabase(), new() { VectorStoreCollectionFactory = new Factory() });
+
+ // Get and create collection if it doesn't exist, using the record definition containing the storage model.
+ var collection = vectorStore.GetCollection("skglossary", s_glossaryDefinition);
+ await collection.CreateCollectionIfNotExistsAsync();
+
+ // Create glossary entries and generate embeddings for them.
+ var glossaryEntries = CreateGlossaryEntries().ToList();
+ var tasks = glossaryEntries.Select(entry => Task.Run(async () =>
+ {
+ entry.Vectors["DefinitionEmbedding"] = await textEmbeddingGenerationService.GenerateEmbeddingAsync((string)entry.Data["Definition"]);
+ }));
+ await Task.WhenAll(tasks);
+
+ // Upsert the glossary entries into the collection and return their keys.
+ var upsertedKeysTasks = glossaryEntries.Select(x => collection.UpsertAsync(x));
+ var upsertedKeys = await Task.WhenAll(upsertedKeysTasks);
+
+ // Retrieve one of the upserted records from the collection.
+ var upsertedRecord = await collection.GetAsync(upsertedKeys.First(), new() { IncludeVectors = true });
+
+ // Write upserted keys and one of the upserted records to the console.
+ Console.WriteLine($"Upserted keys: {string.Join(", ", upsertedKeys)}");
+ Console.WriteLine($"Upserted record: {JsonSerializer.Serialize(upsertedRecord)}");
+ }
+
+ ///
+ /// A custom mapper that maps between the data model and the storage model.
+ ///
+ private sealed class Mapper : IVectorStoreRecordMapper
+ {
+ public (string Key, JsonNode Node) MapFromDataToStorageModel(GenericDataModel dataModel)
+ {
+ var jsonObject = new JsonObject();
+
+ jsonObject.Add("Term", dataModel.Data["Term"].ToString());
+ jsonObject.Add("Definition", dataModel.Data["Definition"].ToString());
+
+ var vector = (ReadOnlyMemory)dataModel.Vectors["DefinitionEmbedding"];
+ var jsonArray = new JsonArray(vector.ToArray().Select(x => JsonValue.Create(x)).ToArray());
+ jsonObject.Add("DefinitionEmbedding", jsonArray);
+
+ return (dataModel.Key, jsonObject);
+ }
+
+ public GenericDataModel MapFromStorageToDataModel((string Key, JsonNode Node) storageModel, StorageToDataModelMapperOptions options)
+ {
+ var dataModel = new GenericDataModel
+ {
+ Key = storageModel.Key,
+ Data = new Dictionary
+ {
+ { "Term", (string)storageModel.Node["Term"]! },
+ { "Definition", (string)storageModel.Node["Definition"]! }
+ },
+ Vectors = new Dictionary
+ {
+ { "DefinitionEmbedding", new ReadOnlyMemory(storageModel.Node["DefinitionEmbedding"]!.AsArray().Select(x => (float)x!).ToArray()) }
+ }
+ };
+
+ return dataModel;
+ }
+ }
+
+ ///
+ /// A factory for creating collections in the vector store
+ ///
+ private sealed class Factory : IRedisVectorStoreRecordCollectionFactory
+ {
+ public IVectorStoreRecordCollection CreateVectorStoreRecordCollection(IDatabase database, string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition)
+ where TKey : notnull
+ where TRecord : class
+ {
+ // If the record definition is the glossary definition and the record type is the generic data model, inject the custom mapper into the collection options.
+ if (vectorStoreRecordDefinition == s_glossaryDefinition && typeof(TRecord) == typeof(GenericDataModel))
+ {
+ var customCollection = new RedisJsonVectorStoreRecordCollection(database, name, new() { VectorStoreRecordDefinition = vectorStoreRecordDefinition, JsonNodeCustomMapper = new Mapper() }) as IVectorStoreRecordCollection;
+ return customCollection!;
+ }
+
+ // Otherwise, just create a standard collection with the default mapper.
+ var collection = new RedisJsonVectorStoreRecordCollection(database, name, new() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }) as IVectorStoreRecordCollection;
+ return collection!;
+ }
+ }
+
+ ///
+ /// Sample generic data model class that can store any data.
+ ///
+ private sealed class GenericDataModel
+ {
+ public string Key { get; set; }
+
+ public Dictionary Data { get; set; }
+
+ public Dictionary Vectors { get; set; }
+ }
+
+ ///
+ /// Create some sample glossary entries using the generic data model.
+ ///
+ /// A list of sample glossary entries.
+ private static IEnumerable CreateGlossaryEntries()
+ {
+ yield return new GenericDataModel
+ {
+ Key = "1",
+ Data = new()
+ {
+ { "Term", "API" },
+ { "Definition", "Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data." }
+ },
+ Vectors = new()
+ };
+
+ yield return new GenericDataModel
+ {
+ Key = "2",
+ Data = new()
+ {
+ { "Term", "Connectors" },
+ { "Definition", "Connectors allow you to integrate with various services provide AI capabilities, including LLM, AudioToText, TextToAudio, Embedding generation, etc." }
+ },
+ Vectors = new()
+ };
+
+ yield return new GenericDataModel
+ {
+ Key = "3",
+ Data = new()
+ {
+ { "Term", "RAG" },
+ { "Definition", "Retrieval Augmented Generation - a term that refers to the process of retrieving additional data to provide as context to an LLM to use when generating a response (completion) to a user’s question (prompt)." }
+ },
+ Vectors = new()
+ };
+ }
+}
diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs
new file mode 100644
index 000000000000..18f0e5b476ca
--- /dev/null
+++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs
@@ -0,0 +1,256 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Memory.VectorStoreFixtures;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Microsoft.SemanticKernel.Connectors.Qdrant;
+using Microsoft.SemanticKernel.Connectors.Redis;
+using Microsoft.SemanticKernel.Data;
+using Microsoft.SemanticKernel.Embeddings;
+using Qdrant.Client;
+using StackExchange.Redis;
+
+namespace Memory;
+
+///
+/// An example showing how to ingest data into a vector store using , or .
+/// Since Redis and Volatile supports string keys and Qdrant supports ulong or Guid keys, this example also shows how you can have common code
+/// that works with both types of keys by using a generic key generator function.
+///
+/// The example shows the following steps:
+/// 1. Register a vector store and embedding generator with the DI container.
+/// 2. Register a class (DataIngestor) with the DI container that uses the vector store and embedding generator to ingest data.
+/// 3. Ingest some data into the vector store.
+/// 4. Read the data back from the vector store.
+///
+/// For some databases in this sample (Redis & Qdrant), you need a local instance of Docker running, since the associated fixtures will try and start containers in the local docker instance to run against.
+///
+[Collection("Sequential")]
+public class VectorStore_DataIngestion_MultiStore(ITestOutputHelper output, VectorStoreRedisContainerFixture redisFixture, VectorStoreQdrantContainerFixture qdrantFixture) : BaseTest(output), IClassFixture, IClassFixture
+{
+ ///
+ /// Example with dependency injection.
+ ///
+ /// The type of database to run the example for.
+ [Theory]
+ [InlineData("Redis")]
+ [InlineData("Qdrant")]
+ [InlineData("Volatile")]
+ public async Task ExampleWithDIAsync(string databaseType)
+ {
+ // Use the kernel for DI purposes.
+ var kernelBuilder = Kernel
+ .CreateBuilder();
+
+ // Register an embedding generation service with the DI container.
+ kernelBuilder.AddAzureOpenAITextEmbeddingGeneration(
+ deploymentName: TestConfiguration.AzureOpenAIEmbeddings.DeploymentName,
+ endpoint: TestConfiguration.AzureOpenAIEmbeddings.Endpoint,
+ apiKey: TestConfiguration.AzureOpenAIEmbeddings.ApiKey);
+
+ // Register the chosen vector store with the DI container and initialize docker containers via the fixtures where needed.
+ if (databaseType == "Redis")
+ {
+ await redisFixture.ManualInitializeAsync();
+ kernelBuilder.AddRedisVectorStore("localhost:6379");
+ }
+ else if (databaseType == "Qdrant")
+ {
+ await qdrantFixture.ManualInitializeAsync();
+ kernelBuilder.AddQdrantVectorStore("localhost");
+ }
+ else if (databaseType == "Volatile")
+ {
+ kernelBuilder.AddVolatileVectorStore();
+ }
+
+ // Register the DataIngestor with the DI container.
+ kernelBuilder.Services.AddTransient();
+
+ // Build the kernel.
+ var kernel = kernelBuilder.Build();
+
+ // Build a DataIngestor object using the DI container.
+ var dataIngestor = kernel.GetRequiredService();
+
+ // Invoke the data ingestor using an appropriate key generator function for each database type.
+ // Redis and Volatile supports string keys, while Qdrant supports ulong or Guid keys, so we use a different key generator for each key type.
+ if (databaseType == "Redis" || databaseType == "Volatile")
+ {
+ await this.UpsertDataAndReadFromVectorStoreAsync(dataIngestor, () => Guid.NewGuid().ToString());
+ }
+ else if (databaseType == "Qdrant")
+ {
+ await this.UpsertDataAndReadFromVectorStoreAsync(dataIngestor, () => Guid.NewGuid());
+ }
+ }
+
+ ///
+ /// Example without dependency injection.
+ ///
+ /// The type of database to run the example for.
+ [Theory]
+ [InlineData("Redis")]
+ [InlineData("Qdrant")]
+ [InlineData("Volatile")]
+ public async Task ExampleWithoutDIAsync(string databaseType)
+ {
+ // Create an embedding generation service.
+ var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService(
+ TestConfiguration.AzureOpenAIEmbeddings.DeploymentName,
+ TestConfiguration.AzureOpenAIEmbeddings.Endpoint,
+ TestConfiguration.AzureOpenAIEmbeddings.ApiKey);
+
+ // Construct the chosen vector store and initialize docker containers via the fixtures where needed.
+ IVectorStore vectorStore;
+ if (databaseType == "Redis")
+ {
+ await redisFixture.ManualInitializeAsync();
+ var database = ConnectionMultiplexer.Connect("localhost:6379").GetDatabase();
+ vectorStore = new RedisVectorStore(database);
+ }
+ else if (databaseType == "Qdrant")
+ {
+ await qdrantFixture.ManualInitializeAsync();
+ var qdrantClient = new QdrantClient("localhost");
+ vectorStore = new QdrantVectorStore(qdrantClient);
+ }
+ else if (databaseType == "Volatile")
+ {
+ vectorStore = new VolatileVectorStore();
+ }
+ else
+ {
+ throw new ArgumentException("Invalid database type.");
+ }
+
+ // Create the DataIngestor.
+ var dataIngestor = new DataIngestor(vectorStore, textEmbeddingGenerationService);
+
+ // Invoke the data ingestor using an appropriate key generator function for each database type.
+ // Redis and Volatile supports string keys, while Qdrant supports ulong or Guid keys, so we use a different key generator for each key type.
+ if (databaseType == "Redis" || databaseType == "Volatile")
+ {
+ await this.UpsertDataAndReadFromVectorStoreAsync(dataIngestor, () => Guid.NewGuid().ToString());
+ }
+ else if (databaseType == "Qdrant")
+ {
+ await this.UpsertDataAndReadFromVectorStoreAsync(dataIngestor, () => Guid.NewGuid());
+ }
+ }
+
+ private async Task UpsertDataAndReadFromVectorStoreAsync(DataIngestor dataIngestor, Func uniqueKeyGenerator)
+ where TKey : notnull
+ {
+ // Ingest some data into the vector store.
+ var upsertedKeys = await dataIngestor.ImportDataAsync(uniqueKeyGenerator);
+
+ // Get one of the upserted records.
+ var upsertedRecord = await dataIngestor.GetGlossaryAsync(upsertedKeys.First());
+
+ // Write upserted keys and one of the upserted records to the console.
+ Console.WriteLine($"Upserted keys: {string.Join(", ", upsertedKeys)}");
+ Console.WriteLine($"Upserted record: {JsonSerializer.Serialize(upsertedRecord)}");
+ }
+
+ ///
+ /// Sample class that does ingestion of sample data into a vector store and allows retrieval of data from the vector store.
+ ///
+ /// The vector store to ingest data into.
+ /// Used to generate embeddings for the data being ingested.
+ private sealed class DataIngestor(IVectorStore vectorStore, ITextEmbeddingGenerationService textEmbeddingGenerationService)
+ {
+ ///
+ /// Create some glossary entries and upsert them into the vector store.
+ ///
+ /// The keys of the upserted glossary entries.
+ /// The type of the keys in the vector store.
+ public async Task> ImportDataAsync(Func uniqueKeyGenerator)
+ where TKey : notnull
+ {
+ // Get and create collection if it doesn't exist.
+ var collection = vectorStore.GetCollection>("skglossary");
+ await collection.CreateCollectionIfNotExistsAsync();
+
+ // Create glossary entries and generate embeddings for them.
+ var glossaryEntries = CreateGlossaryEntries(uniqueKeyGenerator).ToList();
+ var tasks = glossaryEntries.Select(entry => Task.Run(async () =>
+ {
+ entry.DefinitionEmbedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(entry.Definition);
+ }));
+ await Task.WhenAll(tasks);
+
+ // Upsert the glossary entries into the collection and return their keys.
+ var upsertedKeys = glossaryEntries.Select(x => collection.UpsertAsync(x));
+ return await Task.WhenAll(upsertedKeys);
+ }
+
+ ///
+ /// Get a glossary entry from the vector store.
+ ///
+ /// The key of the glossary entry to retrieve.
+ /// The glossary entry.
+ /// The type of the keys in the vector store.
+ public Task?> GetGlossaryAsync(TKey key)
+ where TKey : notnull
+ {
+ var collection = vectorStore.GetCollection>("skglossary");
+ return collection.GetAsync(key, new() { IncludeVectors = true });
+ }
+ }
+
+ ///
+ /// Create some sample glossary entries.
+ ///
+ /// The type of the model key.
+ /// A function that can be used to generate unique keys for the model in the type that the model requires.
+ /// A list of sample glossary entries.
+ private static IEnumerable> CreateGlossaryEntries(Func uniqueKeyGenerator)
+ {
+ yield return new Glossary
+ {
+ Key = uniqueKeyGenerator(),
+ Term = "API",
+ Definition = "Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data."
+ };
+
+ yield return new Glossary
+ {
+ Key = uniqueKeyGenerator(),
+ Term = "Connectors",
+ Definition = "Connectors allow you to integrate with various services provide AI capabilities, including LLM, AudioToText, TextToAudio, Embedding generation, etc."
+ };
+
+ yield return new Glossary
+ {
+ Key = uniqueKeyGenerator(),
+ Term = "RAG",
+ Definition = "Retrieval Augmented Generation - a term that refers to the process of retrieving additional data to provide as context to an LLM to use when generating a response (completion) to a user’s question (prompt)."
+ };
+ }
+
+ ///
+ /// Sample model class that represents a glossary entry.
+ ///
+ ///
+ /// Note that each property is decorated with an attribute that specifies how the property should be treated by the vector store.
+ /// This allows us to create a collection in the vector store and upsert and retrieve instances of this class without any further configuration.
+ ///
+ /// The type of the model key.
+ private sealed class Glossary
+ {
+ [VectorStoreRecordKey]
+ public TKey Key { get; set; }
+
+ [VectorStoreRecordData]
+ public string Term { get; set; }
+
+ [VectorStoreRecordData]
+ public string Definition { get; set; }
+
+ [VectorStoreRecordVector(1536)]
+ public ReadOnlyMemory DefinitionEmbedding { get; set; }
+ }
+}
diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs
new file mode 100644
index 000000000000..341e5c2bbda2
--- /dev/null
+++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Memory.VectorStoreFixtures;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Microsoft.SemanticKernel.Connectors.Qdrant;
+using Microsoft.SemanticKernel.Data;
+using Microsoft.SemanticKernel.Embeddings;
+using Qdrant.Client;
+
+namespace Memory;
+
+///
+/// A simple example showing how to ingest data into a vector store using .
+///
+/// The example shows the following steps:
+/// 1. Create an embedding generator.
+/// 2. Create a Qdrant Vector Store.
+/// 3. Ingest some data into the vector store.
+/// 4. Read the data back from the vector store.
+///
+/// You need a local instance of Docker running, since the associated fixture will try and start a Qdrant container in the local docker instance to run against.
+///
+[Collection("Sequential")]
+public class VectorStore_DataIngestion_Simple(ITestOutputHelper output, VectorStoreQdrantContainerFixture qdrantFixture) : BaseTest(output), IClassFixture
+{
+ [Fact]
+ public async Task ExampleAsync()
+ {
+ // Create an embedding generation service.
+ var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService(
+ TestConfiguration.AzureOpenAIEmbeddings.DeploymentName,
+ TestConfiguration.AzureOpenAIEmbeddings.Endpoint,
+ TestConfiguration.AzureOpenAIEmbeddings.ApiKey);
+
+ // Initiate the docker container and construct the vector store.
+ await qdrantFixture.ManualInitializeAsync();
+ var vectorStore = new QdrantVectorStore(new QdrantClient("localhost"));
+
+ // Get and create collection if it doesn't exist.
+ var collection = vectorStore.GetCollection("skglossary");
+ await collection.CreateCollectionIfNotExistsAsync();
+
+ // Create glossary entries and generate embeddings for them.
+ var glossaryEntries = CreateGlossaryEntries().ToList();
+ var tasks = glossaryEntries.Select(entry => Task.Run(async () =>
+ {
+ entry.DefinitionEmbedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(entry.Definition);
+ }));
+ await Task.WhenAll(tasks);
+
+ // Upsert the glossary entries into the collection and return their keys.
+ var upsertedKeysTasks = glossaryEntries.Select(x => collection.UpsertAsync(x));
+ var upsertedKeys = await Task.WhenAll(upsertedKeysTasks);
+
+ // Retrieve one of the upserted records from the collection.
+ var upsertedRecord = await collection.GetAsync(upsertedKeys.First(), new() { IncludeVectors = true });
+
+ // Write upserted keys and one of the upserted records to the console.
+ Console.WriteLine($"Upserted keys: {string.Join(", ", upsertedKeys)}");
+ Console.WriteLine($"Upserted record: {JsonSerializer.Serialize(upsertedRecord)}");
+ }
+
+ ///
+ /// Sample model class that represents a glossary entry.
+ ///
+ ///
+ /// Note that each property is decorated with an attribute that specifies how the property should be treated by the vector store.
+ /// This allows us to create a collection in the vector store and upsert and retrieve instances of this class without any further configuration.
+ ///
+ private sealed class Glossary
+ {
+ [VectorStoreRecordKey]
+ public ulong Key { get; set; }
+
+ [VectorStoreRecordData]
+ public string Term { get; set; }
+
+ [VectorStoreRecordData]
+ public string Definition { get; set; }
+
+ [VectorStoreRecordVector(1536)]
+ public ReadOnlyMemory DefinitionEmbedding { get; set; }
+ }
+
+ ///
+ /// Create some sample glossary entries.
+ ///
+ /// A list of sample glossary entries.
+ private static IEnumerable CreateGlossaryEntries()
+ {
+ yield return new Glossary
+ {
+ Key = 1,
+ Term = "API",
+ Definition = "Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data."
+ };
+
+ yield return new Glossary
+ {
+ Key = 2,
+ Term = "Connectors",
+ Definition = "Connectors allow you to integrate with various services provide AI capabilities, including LLM, AudioToText, TextToAudio, Embedding generation, etc."
+ };
+
+ yield return new Glossary
+ {
+ Key = 3,
+ Term = "RAG",
+ Definition = "Retrieval Augmented Generation - a term that refers to the process of retrieving additional data to provide as context to an LLM to use when generating a response (completion) to a user’s question (prompt)."
+ };
+ }
+}
diff --git a/dotnet/samples/Concepts/Plugins/OpenApiPlugin_CustomHttpContentReader.cs b/dotnet/samples/Concepts/Plugins/OpenApiPlugin_CustomHttpContentReader.cs
new file mode 100644
index 000000000000..829e5a42abb3
--- /dev/null
+++ b/dotnet/samples/Concepts/Plugins/OpenApiPlugin_CustomHttpContentReader.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Plugins.OpenApi;
+
+namespace Plugins;
+
+///
+/// Sample shows how to register a custom HTTP content reader for an Open API plugin.
+///
+public sealed class CustomHttpContentReaderForOpenApiPlugin(ITestOutputHelper output) : BaseTest(output)
+{
+ [Fact]
+ public async Task ShowReadingJsonAsStreamAsync()
+ {
+ var kernel = new Kernel();
+
+ // Register the custom HTTP content reader
+ var executionParameters = new OpenApiFunctionExecutionParameters() { HttpResponseContentReader = ReadHttpResponseContentAsync };
+
+ // Create OpenAPI plugin
+ var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("RepairService", "Resources/Plugins/RepairServicePlugin/repair-service.json", executionParameters);
+
+ // Create a repair so it can be read as a stream in the following step
+ var arguments = new KernelArguments
+ {
+ ["title"] = "The Case of the Broken Gizmo",
+ ["description"] = "It's broken. Send help!",
+ ["assignedTo"] = "Tech Magician"
+ };
+ var createResult = await plugin["createRepair"].InvokeAsync(kernel, arguments);
+ Console.WriteLine(createResult.ToString());
+
+ // List relevant repairs
+ arguments = new KernelArguments
+ {
+ ["assignedTo"] = "Tech Magician"
+ };
+ var listResult = await plugin["listRepairs"].InvokeAsync(kernel, arguments);
+ using var reader = new StreamReader((Stream)listResult.GetValue()!.Content!);
+ var content = await reader.ReadToEndAsync();
+ var repairs = JsonSerializer.Deserialize(content);
+ Console.WriteLine(content);
+
+ // Delete the repair
+ arguments = new KernelArguments
+ {
+ ["id"] = repairs!.Where(r => r.AssignedTo == "Tech Magician").First().Id.ToString()
+ };
+ var deleteResult = await plugin["deleteRepair"].InvokeAsync(kernel, arguments);
+ Console.WriteLine(deleteResult.ToString());
+ }
+
+ ///
+ /// A custom HTTP content reader to change the default behavior of reading HTTP content.
+ ///
+ /// The HTTP response content reader context.
+ /// The cancellation token.
+ /// The HTTP response content.
+ private static async Task