diff --git a/.github/labeler.yml b/.github/labeler.yml
index 64d48e798940..2e0a70c613dc 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,43 +1,56 @@
# Add 'kernel' label to any change within Connectors, Extensions, Skills, and tests directories
kernel:
- - dotnet/src/Connectors/**/*
- - dotnet/src/Extensions/**/*
- - dotnet/src/Skills/**/*
- - dotnet/src/IntegrationTests/**/*
- - dotnet/src/SemanticKernel.UnitTests/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - dotnet/src/Connectors/**/*
+ - dotnet/src/Extensions/**/*
+ - dotnet/src/Skills/**/*
+ - dotnet/src/IntegrationTests/**/*
+ - dotnet/src/SemanticKernel.UnitTests/**/*
# Add 'kernel.core' label to any change within the 'SemanticKernel', 'SemanticKernel.Abstractions', or 'SemanticKernel.MetaPackage' directories
kernel.core:
- - dotnet/src/SemanticKernel/**/*
- - dotnet/src/SemanticKernel.Abstractions/**/*
- - dotnet/src/SemanticKernel.MetaPackage/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - dotnet/src/SemanticKernel/**/*
+ - dotnet/src/SemanticKernel.Abstractions/**/*
+ - dotnet/src/SemanticKernel.MetaPackage/**/*
# Add 'python' label to any change within the 'python' directory
python:
- - python/**/*
+ - changed-files:
+ - any-glob-to-any-file: python/**/*
# Add 'java' label to any change within the 'java' directory
java:
- - java/**/*
+ - changed-files:
+ - any-glob-to-any-file: java/**/*
# Add 'samples' label to any change within the 'samples' directory
samples:
- - samples/**/*
+ - changed-files:
+ - any-glob-to-any-file: samples/**/*
# Add '.NET' label to any change within samples or kernel 'dotnet' directories.
.NET:
- - dotnet/**/*
+ - changed-files:
+ - any-glob-to-any-file: dotnet/**/*
# Add 'copilot chat' label to any change within the 'samples/apps/copilot-chat-app' directory
copilot chat:
- - samples/apps/copilot-chat-app/**/*
+ - changed-files:
+ - any-glob-to-any-file: samples/apps/copilot-chat-app/**/*
# Add 'documentation' label to any change within the 'docs' directory, or any '.md' files
documentation:
- - docs/**/*
- - '**/*.md'
+ - changed-files:
+ - any-glob-to-any-file:
+ - docs/**/*
+ - '**/*.md'
# Add 'memory' label to any memory connectors in dotnet/ or python/
memory:
- - dotnet/src/Connectors/Connectors.Memory.*/**/*
- - python/semantic_kernel/connectors/memory/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - dotnet/src/Connectors/Connectors.Memory.*/**/*
+ - python/semantic_kernel/connectors/memory/**/*
diff --git a/.github/workflows/close-inactive-issues.yml b/.github/workflows/close-inactive-issues.yml
index 10244c7609d9..4e6ebccef628 100644
--- a/.github/workflows/close-inactive-issues.yml
+++ b/.github/workflows/close-inactive-issues.yml
@@ -10,7 +10,7 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@v5
+ - uses: actions/stale@v10
with:
days-before-issue-stale: 90
days-before-issue-close: 14
diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml
index d7277dcdb76d..6395f6a0b9b8 100644
--- a/.github/workflows/dotnet-build-and-test.yml
+++ b/.github/workflows/dotnet-build-and-test.yml
@@ -184,7 +184,7 @@ jobs:
# Generate test reports and check coverage
- name: Generate test reports
- uses: danielpalme/ReportGenerator-GitHub-Action@5.4.12
+ uses: danielpalme/ReportGenerator-GitHub-Action@5.4.15
with:
reports: "./TestResults/Coverage/**/coverage.cobertura.xml"
targetdir: "./TestResults/Reports"
@@ -232,13 +232,13 @@ jobs:
- name: Fail workflow if tests failed
id: check_tests_failed
if: contains(join(needs.*.result, ','), 'failure')
- uses: actions/github-script@v6
+ uses: actions/github-script@v8
with:
script: core.setFailed('Integration Tests Failed!')
- name: Fail workflow if tests cancelled
id: check_tests_cancelled
if: contains(join(needs.*.result, ','), 'cancelled')
- uses: actions/github-script@v6
+ uses: actions/github-script@v8
with:
script: core.setFailed('Integration Tests Cancelled!')
diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml
index 9cbe4e3ab357..1e0cd992e038 100644
--- a/.github/workflows/dotnet-format.yml
+++ b/.github/workflows/dotnet-format.yml
@@ -53,11 +53,14 @@ jobs:
- name: Find csproj files
id: find-csproj
if: github.event_name != 'pull_request' || steps.changed-files.outputs.added_modified != '' || steps.changed-files.outcome == 'failure'
+ env:
+ ADDED_MODIFIED: ${{ steps.changed-files.outputs.added_modified }}
run: |
csproj_files=()
exclude_files=("Experimental.Orchestration.Flow.csproj" "Experimental.Orchestration.Flow.UnitTests.csproj" "Experimental.Orchestration.Flow.IntegrationTests.csproj")
+ set -f
if [[ ${{ steps.changed-files.outcome }} == 'success' ]]; then
- for file in ${{ steps.changed-files.outputs.added_modified }}; do
+ for file in $ADDED_MODIFIED; do
echo "$file was changed"
dir="./$file"
while [[ $dir != "." && $dir != "/" && $dir != $GITHUB_WORKSPACE ]]; do
@@ -78,7 +81,8 @@ jobs:
fi
csproj_files=($(printf "%s\n" "${csproj_files[@]}" | sort -u))
echo "Found ${#csproj_files[@]} unique csproj/slnx files: ${csproj_files[*]}"
- echo "::set-output name=csproj_files::${csproj_files[*]}"
+ echo "csproj_files=${csproj_files[*]}" >> "$GITHUB_OUTPUT"
+ set +f
- name: Pull container dotnet/sdk:${{ matrix.dotnet }}
if: steps.find-csproj.outputs.csproj_files != ''
@@ -87,8 +91,11 @@ jobs:
# This step will run dotnet format on each of the unique csproj files and fail if any changes are made
- name: Run dotnet format
if: steps.find-csproj.outputs.csproj_files != ''
+ env:
+ CSPROJ_FILES: ${{ steps.find-csproj.outputs.csproj_files }}
run: |
- for csproj in ${{ steps.find-csproj.outputs.csproj_files }}; do
+ set -f
+ for csproj in $CSPROJ_FILES; do
echo "Running dotnet format on $csproj"
- docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "dotnet format $csproj --verify-no-changes --verbosity diagnostic"
+ docker run --rm -v "$(pwd):/app" -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} dotnet format "$csproj" --verify-no-changes --verbosity diagnostic
done
diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml
index a011f3b4e81f..cf901ade0cb3 100644
--- a/.github/workflows/label-issues.yml
+++ b/.github/workflows/label-issues.yml
@@ -13,7 +13,7 @@ jobs:
permissions:
issues: write
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v8
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml
index 61e77b7ff18c..4aea432e31c0 100644
--- a/.github/workflows/label-pr.yml
+++ b/.github/workflows/label-pr.yml
@@ -16,6 +16,6 @@ jobs:
pull-requests: write
steps:
- - uses: actions/labeler@v4
+ - uses: actions/labeler@v6
with:
repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}"
diff --git a/.github/workflows/label-title-prefix.yml b/.github/workflows/label-title-prefix.yml
index b0c01aad3fcd..81369deedf25 100644
--- a/.github/workflows/label-title-prefix.yml
+++ b/.github/workflows/label-title-prefix.yml
@@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v8
name: "Issue/PR: update title"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml
index a97ff57c5d27..6d08cafa2bcd 100644
--- a/.github/workflows/python-integration-tests.yml
+++ b/.github/workflows/python-integration-tests.yml
@@ -534,14 +534,14 @@ jobs:
- name: Fail workflow if tests failed
id: check_tests_failed
if: contains(join(needs.*.result, ','), 'failure')
- uses: actions/github-script@v6
+ uses: actions/github-script@v8
with:
script: core.setFailed('Integration Tests Failed!')
- name: Fail workflow if tests cancelled
id: check_tests_cancelled
if: contains(join(needs.*.result, ','), 'cancelled')
- uses: actions/github-script@v6
+ uses: actions/github-script@v8
with:
script: core.setFailed('Integration Tests Cancelled!')
diff --git a/.github/workflows/typos.yaml b/.github/workflows/typos.yaml
index 065c6b4cd97e..9a27ab8d5dd6 100644
--- a/.github/workflows/typos.yaml
+++ b/.github/workflows/typos.yaml
@@ -22,10 +22,18 @@ jobs:
- name: Check out code
uses: actions/checkout@v5
with:
+ fetch-depth: 0
persist-credentials: false
+ - name: Get changed files
+ id: changed-files
+ if: github.event_name == 'pull_request'
+ uses: jitterbit/get-changed-files@v1
+ continue-on-error: true
+
- name: Use custom config file
- uses: crate-ci/typos@v1.36.2
+ uses: crate-ci/typos@v1.44.0
with:
+ files: ${{ github.event_name == 'pull_request' && steps.changed-files.outputs.added_modified || '.' }}
config: .github/_typos.toml
write_changes: false
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 29d44c8fc753..4165274cba62 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -6,21 +6,21 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
-
-
+
+
@@ -57,7 +57,9 @@
+
+
@@ -87,13 +89,12 @@
-
-
+
-
+
@@ -106,8 +107,8 @@
-
-
+
+
@@ -115,7 +116,7 @@
-
+
@@ -126,7 +127,7 @@
-
+
@@ -149,10 +150,11 @@
+
-
+
@@ -173,9 +175,7 @@
-
-
-
+
@@ -202,15 +202,14 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -261,4 +260,4 @@
-
\ No newline at end of file
+
diff --git a/dotnet/global.json b/dotnet/global.json
index 869bf826236f..5411073f86a8 100644
--- a/dotnet/global.json
+++ b/dotnet/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "10.0.301",
+ "version": "10.0.302",
"rollForward": "major",
"allowPrerelease": false
}
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index 6877b3197871..8f7576e7379c 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -1,7 +1,7 @@
- 1.77.0
+ 1.79.0
$(VersionPrefix)-$(VersionSuffix)
$(VersionPrefix)
@@ -9,7 +9,7 @@
true
- 1.76.0
+ 1.78.0
$(NoWarn);CP0003
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_ContextualFunctionSelection.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_ContextualFunctionSelection.cs
index 94d800985fc1..4b28a0025992 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_ContextualFunctionSelection.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_ContextualFunctionSelection.cs
@@ -2,10 +2,10 @@
using Azure.AI.OpenAI;
using Azure.Identity;
+using CommunityToolkit.VectorData.InMemory;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
-using Microsoft.SemanticKernel.Connectors.InMemory;
using Microsoft.SemanticKernel.Functions;
namespace Agents;
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Rag.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Rag.cs
index 8d7cf969e319..ef9bbd602fc5 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_Rag.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Rag.cs
@@ -2,11 +2,11 @@
using Azure.AI.OpenAI;
using Azure.Identity;
+using CommunityToolkit.VectorData.InMemory;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
-using Microsoft.SemanticKernel.Connectors.InMemory;
using Microsoft.SemanticKernel.Data;
namespace Agents;
diff --git a/dotnet/samples/Concepts/Caching/SemanticCachingWithFilters.cs b/dotnet/samples/Concepts/Caching/SemanticCachingWithFilters.cs
index 8c7115ba1534..8549d40d14e2 100644
--- a/dotnet/samples/Concepts/Caching/SemanticCachingWithFilters.cs
+++ b/dotnet/samples/Concepts/Caching/SemanticCachingWithFilters.cs
@@ -293,7 +293,7 @@ private sealed class CacheRecord
[VectorStoreData]
public string Result { get; set; }
- [VectorStoreVector(Dimensions: 1536)]
+ [VectorStoreVector(1536)]
public ReadOnlyMemory PromptEmbedding { get; set; }
}
diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj
index ad6b3fe12a59..5345cdface12 100644
--- a/dotnet/samples/Concepts/Concepts.csproj
+++ b/dotnet/samples/Concepts/Concepts.csproj
@@ -9,6 +9,8 @@
true
$(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0101,SKEXP0110,OPENAI001,CA1724,IDE1006,IDE0009,MEVD9000
+
+ true
Library
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
@@ -49,15 +51,12 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/dotnet/samples/Concepts/Memory/HuggingFace_TextEmbeddingCustomHttpHandler.cs b/dotnet/samples/Concepts/Memory/HuggingFace_TextEmbeddingCustomHttpHandler.cs
index 7e27f0a25bc3..06068932ed1c 100644
--- a/dotnet/samples/Concepts/Memory/HuggingFace_TextEmbeddingCustomHttpHandler.cs
+++ b/dotnet/samples/Concepts/Memory/HuggingFace_TextEmbeddingCustomHttpHandler.cs
@@ -1,9 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
+using CommunityToolkit.VectorData.InMemory;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.HuggingFace;
-using Microsoft.SemanticKernel.Connectors.InMemory;
using Microsoft.SemanticKernel.Embeddings;
#pragma warning disable CS8602 // Dereference of a possibly null reference.
@@ -52,7 +52,7 @@ public class Record
[VectorStoreData]
public string Text { get; set; }
- [VectorStoreVector(Dimensions: 768)]
+ [VectorStoreVector(768)]
public string Embedding { get; set; }
}
diff --git a/dotnet/samples/Concepts/Memory/VectorStoreLangchainInterop/PineconeFactory.cs b/dotnet/samples/Concepts/Memory/VectorStoreLangchainInterop/PineconeFactory.cs
deleted file mode 100644
index 2aa8c73a19c9..000000000000
--- a/dotnet/samples/Concepts/Memory/VectorStoreLangchainInterop/PineconeFactory.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.Extensions.VectorData;
-using Microsoft.SemanticKernel.Connectors.Pinecone;
-using Pinecone;
-
-namespace Memory.VectorStoreLangchainInterop;
-
-///
-/// Contains a factory method that can be used to create a Pinecone vector store that is compatible with datasets ingested using Langchain.
-///
-///
-/// This class is used with the sample.
-///
-public static class PineconeFactory
-{
- ///
- /// Record definition that matches the storage format used by Langchain for Pinecone.
- ///
- private static readonly VectorStoreCollectionDefinition s_definition = new()
- {
- Properties =
- [
- new VectorStoreKeyProperty("Key", typeof(string)),
- new VectorStoreDataProperty("Content", typeof(string)) { StorageName = "text" },
- new VectorStoreDataProperty("Source", typeof(string)) { StorageName = "source" },
- new VectorStoreVectorProperty("Embedding", typeof(ReadOnlyMemory), 1536) { StorageName = "embedding" }
- ]
- };
-
- ///
- /// Create a new Pinecone-backed that can be used to read data that was ingested using Langchain.
- ///
- /// Pinecone client that can be used to manage the collections and points in a Pinecone store.
- /// The .
- public static VectorStore CreatePineconeLangchainInteropVectorStore(PineconeClient pineconeClient)
- => new PineconeLangchainInteropVectorStore(new PineconeVectorStore(pineconeClient), pineconeClient);
-
- private sealed class PineconeLangchainInteropVectorStore(
- VectorStore innerStore,
- PineconeClient pineconeClient)
- : VectorStore
- {
- private readonly PineconeClient _pineconeClient = pineconeClient;
-
- public override VectorStoreCollection GetCollection(string name, VectorStoreCollectionDefinition? definition = null)
- {
- if (typeof(TKey) != typeof(string) || typeof(TRecord) != typeof(LangchainDocument))
- {
- throw new NotSupportedException("This VectorStore is only usable with string keys and LangchainDocument record types");
- }
-
- // Create a Pinecone collection and pass in our custom record definition that matches
- // the schema used by Langchain so that the default mapper can use the storage names
- // in it, to map to the storage scheme.
- return (new PineconeCollection(
- _pineconeClient,
- name,
- new()
- {
- Definition = s_definition
- }) as VectorStoreCollection)!;
- }
-
- public override VectorStoreCollection
@@ -15,15 +17,13 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStartedWithTextSearch/GettingStartedWithTextSearch.csproj b/dotnet/samples/GettingStartedWithTextSearch/GettingStartedWithTextSearch.csproj
index 9eab7a6c81d7..29ac5e5b7e0b 100644
--- a/dotnet/samples/GettingStartedWithTextSearch/GettingStartedWithTextSearch.csproj
+++ b/dotnet/samples/GettingStartedWithTextSearch/GettingStartedWithTextSearch.csproj
@@ -35,7 +35,7 @@
-
+
diff --git a/dotnet/samples/GettingStartedWithTextSearch/InMemoryVectorStoreFixture.cs b/dotnet/samples/GettingStartedWithTextSearch/InMemoryVectorStoreFixture.cs
index c409d53b8260..6cd2d692c1d5 100644
--- a/dotnet/samples/GettingStartedWithTextSearch/InMemoryVectorStoreFixture.cs
+++ b/dotnet/samples/GettingStartedWithTextSearch/InMemoryVectorStoreFixture.cs
@@ -1,10 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Reflection;
+using CommunityToolkit.VectorData.InMemory;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.VectorData;
-using Microsoft.SemanticKernel.Connectors.InMemory;
using Microsoft.SemanticKernel.Data;
using OpenAI;
diff --git a/dotnet/samples/GettingStartedWithVectorStores/GettingStartedWithVectorStores.csproj b/dotnet/samples/GettingStartedWithVectorStores/GettingStartedWithVectorStores.csproj
index b08f7cf0ce95..d911327ee3d6 100644
--- a/dotnet/samples/GettingStartedWithVectorStores/GettingStartedWithVectorStores.csproj
+++ b/dotnet/samples/GettingStartedWithVectorStores/GettingStartedWithVectorStores.csproj
@@ -32,9 +32,9 @@
-
-
-
+
+
+
diff --git a/dotnet/samples/GettingStartedWithVectorStores/Glossary.cs b/dotnet/samples/GettingStartedWithVectorStores/Glossary.cs
index 1516c0d35230..0d5fadb3b2fb 100644
--- a/dotnet/samples/GettingStartedWithVectorStores/Glossary.cs
+++ b/dotnet/samples/GettingStartedWithVectorStores/Glossary.cs
@@ -25,6 +25,6 @@ internal sealed class Glossary
[VectorStoreData]
public string Definition { get; set; }
- [VectorStoreVector(Dimensions: 1536)]
+ [VectorStoreVector(1536)]
public ReadOnlyMemory DefinitionEmbedding { get; set; }
}
diff --git a/dotnet/samples/GettingStartedWithVectorStores/Step1_Ingest_Data.cs b/dotnet/samples/GettingStartedWithVectorStores/Step1_Ingest_Data.cs
index 3f37685eebf0..3b98cc037344 100644
--- a/dotnet/samples/GettingStartedWithVectorStores/Step1_Ingest_Data.cs
+++ b/dotnet/samples/GettingStartedWithVectorStores/Step1_Ingest_Data.cs
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
+using CommunityToolkit.VectorData.InMemory;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
-using Microsoft.SemanticKernel.Connectors.InMemory;
namespace GettingStartedWithVectorStores;
diff --git a/dotnet/samples/GettingStartedWithVectorStores/Step2_Vector_Search.cs b/dotnet/samples/GettingStartedWithVectorStores/Step2_Vector_Search.cs
index 11cec7a34ca8..a8079a0732e3 100644
--- a/dotnet/samples/GettingStartedWithVectorStores/Step2_Vector_Search.cs
+++ b/dotnet/samples/GettingStartedWithVectorStores/Step2_Vector_Search.cs
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
+using CommunityToolkit.VectorData.InMemory;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
-using Microsoft.SemanticKernel.Connectors.InMemory;
namespace GettingStartedWithVectorStores;
diff --git a/dotnet/samples/GettingStartedWithVectorStores/Step3_Switch_VectorStore.cs b/dotnet/samples/GettingStartedWithVectorStores/Step3_Switch_VectorStore.cs
index da035fe5b5b2..1e8706fa9c76 100644
--- a/dotnet/samples/GettingStartedWithVectorStores/Step3_Switch_VectorStore.cs
+++ b/dotnet/samples/GettingStartedWithVectorStores/Step3_Switch_VectorStore.cs
@@ -2,8 +2,8 @@
using Azure;
using Azure.Search.Documents.Indexes;
-using Microsoft.SemanticKernel.Connectors.AzureAISearch;
-using Microsoft.SemanticKernel.Connectors.Redis;
+using CommunityToolkit.VectorData.AzureAISearch;
+using CommunityToolkit.VectorData.Redis;
using StackExchange.Redis;
namespace GettingStartedWithVectorStores;
diff --git a/dotnet/samples/GettingStartedWithVectorStores/Step4_Use_DynamicDataModel.cs b/dotnet/samples/GettingStartedWithVectorStores/Step4_Use_DynamicDataModel.cs
index 3d93a1244818..6d433b594d1d 100644
--- a/dotnet/samples/GettingStartedWithVectorStores/Step4_Use_DynamicDataModel.cs
+++ b/dotnet/samples/GettingStartedWithVectorStores/Step4_Use_DynamicDataModel.cs
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
+using CommunityToolkit.VectorData.Redis;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
-using Microsoft.SemanticKernel.Connectors.Redis;
using StackExchange.Redis;
namespace GettingStartedWithVectorStores;
diff --git a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs
index c765bf1d678d..c66dbf71ea07 100644
--- a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs
+++ b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs
@@ -188,6 +188,57 @@ public async Task GetTextContentsExecutionSettingsMustBeSentAsync()
Assert.Equal(ollamaExecutionSettings.TopK, requestPayload.Options.TopK);
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task GetTextContentsShouldSendThinkSettingAsync(bool thinkValue)
+ {
+ // Arrange
+ var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient);
+ var settings = new OllamaPromptExecutionSettings { Think = thinkValue };
+
+ // Act
+ await sut.GetTextContentsAsync("Any prompt", settings);
+
+ // Assert
+ var requestPayload = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent);
+ Assert.NotNull(requestPayload);
+ Assert.Equal(thinkValue, (bool?)requestPayload.Think);
+ }
+
+ [Fact]
+ public async Task GetTextContentsShouldNotSendThinkWhenNotSetAsync()
+ {
+ // Arrange
+ var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient);
+
+ // Act
+ await sut.GetTextContentsAsync("Any prompt");
+
+ // Assert
+ var requestPayload = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent);
+ Assert.NotNull(requestPayload);
+ Assert.Null(requestPayload.Think);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task GetStreamingTextContentsShouldSendThinkSettingAsync(bool thinkValue)
+ {
+ // Arrange
+ var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient);
+ var settings = new OllamaPromptExecutionSettings { Think = thinkValue };
+
+ // Act
+ await sut.GetStreamingTextContentsAsync("Any prompt", settings).GetAsyncEnumerator().MoveNextAsync();
+
+ // Assert
+ var requestPayload = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent);
+ Assert.NotNull(requestPayload);
+ Assert.Equal(thinkValue, (bool?)requestPayload.Think);
+ }
+
///
/// Disposes resources used by this class.
///
diff --git a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs
index fb41f2f991cc..fff0aef5921c 100644
--- a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs
+++ b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs
@@ -41,6 +41,7 @@ public void FromExecutionSettingsWhenNullShouldReturnDefault()
Assert.Null(ollamaExecutionSettings.Temperature);
Assert.Null(ollamaExecutionSettings.TopP);
Assert.Null(ollamaExecutionSettings.TopK);
+ Assert.Null(ollamaExecutionSettings.Think);
}
[Fact]
@@ -187,6 +188,45 @@ public void ClonePreservesServiceId()
Assert.Equal(testSettings.Temperature, cloned.Temperature);
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ThinkPropertyRoundTripsViaSerialization(bool thinkValue)
+ {
+ // Arrange
+ string jsonSettings = $$"""{ "think": {{thinkValue.ToString().ToLowerInvariant()}} }""";
+
+ // Act
+ var executionSettings = JsonSerializer.Deserialize(jsonSettings);
+
+ // Assert
+ Assert.Equal(thinkValue, executionSettings!.Think);
+ }
+
+ [Fact]
+ public void ThinkPropertyIsPreservedByClone()
+ {
+ // Arrange
+ var settings = new OllamaPromptExecutionSettings { Think = false };
+
+ // Act
+ var clone = (OllamaPromptExecutionSettings)settings.Clone();
+
+ // Assert
+ Assert.Equal(false, clone.Think);
+ }
+
+ [Fact]
+ public void ThinkPropertyThrowsWhenFrozen()
+ {
+ // Arrange
+ var settings = new OllamaPromptExecutionSettings();
+ settings.Freeze();
+
+ // Act & Assert
+ Assert.Throws(() => settings.Think = true);
+ }
+
[Fact]
public void PromptExecutionSettingsFreezeWorksAsExpected()
{
diff --git a/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs b/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs
index d149d7a1b3fa..b965c90b690c 100644
--- a/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs
+++ b/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs
@@ -148,7 +148,8 @@ private static GenerateRequest CreateRequest(OllamaPromptExecutionSettings setti
NumPredict = settings.NumPredict
},
Model = selectedModel,
- Stream = true
+ Stream = true,
+ Think = settings.Think.HasValue ? (OllamaSharp.Models.Chat.ThinkValue?)settings.Think.Value : null
};
return request;
diff --git a/dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs
index 1b49aa99d97d..898dfa6a5ab4 100644
--- a/dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs
+++ b/dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs
@@ -131,6 +131,30 @@ public int? NumPredict
}
}
+ ///
+ /// Enables or disables thinking for reasoning models such as deepseek-r1, qwen3, and phi4-reasoning.
+ /// Set to false to disable thinking and receive a standard response when using a model that
+ /// enables thinking by default. Set to true to explicitly enable thinking.
+ /// When null (the default), the model's own default behavior is used.
+ ///
+ ///
+ /// When thinking is active, the model's reasoning output lands in a separate thinking stream
+ /// rather than in the main response content. Setting this to false suppresses thinking
+ /// so that all output appears in the standard response field.
+ ///
+ [JsonPropertyName("think")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? Think
+ {
+ get => this._think;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._think = value;
+ }
+ }
+
///
public override void Freeze()
{
@@ -161,6 +185,7 @@ public override PromptExecutionSettings Clone()
NumPredict = this.NumPredict,
Stop = this.Stop is not null ? new List(this.Stop) : null,
FunctionChoiceBehavior = this.FunctionChoiceBehavior,
+ Think = this.Think,
};
}
@@ -171,6 +196,7 @@ public override PromptExecutionSettings Clone()
private float? _topP;
private int? _topK;
private int? _numPredict;
+ private bool? _think;
#endregion
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs
index 57b7700a0595..1ffbbcce1dcf 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs
@@ -1981,6 +1981,16 @@ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext co
{ "{\"voice\":\"echo\",\"format\":\"opus\"}", "{\"voice\":\"echo\",\"format\":\"opus\"}" },
};
+ public static TheoryData AudioMimeTypeMappingData => new()
+ {
+ { ChatOutputAudioFormat.Wav, "audio/wav" },
+ { ChatOutputAudioFormat.Aac, "audio/aac" },
+ { ChatOutputAudioFormat.Mp3, "audio/mp3" },
+ { ChatOutputAudioFormat.Opus, "audio/opus" },
+ { ChatOutputAudioFormat.Flac, "audio/flac" },
+ { ChatOutputAudioFormat.Pcm16, "audio/pcm16" },
+ };
+
#pragma warning disable CS8618, CA1812
private sealed class MathReasoning
{
@@ -2191,6 +2201,56 @@ public async Task ItHandlesAudioContentWithMetadataInResponseAsync()
// The ExpiresAt value is converted to a DateTime object, so we can't directly compare it to the Unix timestamp
}
+ [Theory]
+ [MemberData(nameof(AudioMimeTypeMappingData))]
+ public async Task ItMapsAudioOutputFormatToCorrectMimeTypeAsync(ChatOutputAudioFormat format, string expectedMimeType)
+ {
+ // Arrange
+ var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-4o", apiKey: "NOKEY", httpClient: this._httpClient);
+
+ var responseJson = """
+ {
+ "model": "gpt-4o",
+ "choices": [
+ {
+ "message": {
+ "role": "assistant",
+ "content": "This is the text response.",
+ "audio": {
+ "data": "AQIDBA=="
+ }
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 10,
+ "completion_tokens": 20,
+ "total_tokens": 30
+ }
+ }
+ """;
+
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ { Content = new StringContent(responseJson) };
+
+ var settings = new OpenAIPromptExecutionSettings
+ {
+ Modalities = ChatResponseModalities.Text | ChatResponseModalities.Audio,
+ Audio = new ChatAudioOptions(ChatOutputAudioVoice.Alloy, format)
+ };
+
+ // Act
+ var result = await chatCompletion.GetChatMessageContentAsync(this._chatHistoryForTest, settings);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(2, result.Items.Count);
+ var audioContent = result.Items[1] as AudioContent;
+ Assert.NotNull(audioContent);
+ Assert.Equal(expectedMimeType, audioContent.MimeType);
+ }
+
[Fact]
public async Task GetChatMessageContentsThrowsExceptionWithEmptyBinaryContentAsync()
{
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs
index 88ace29aff4d..2e9e77cf04eb 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs
@@ -1006,9 +1006,9 @@ private OpenAIChatMessageContent CreateChatMessageContent(OAIChat.ChatCompletion
return "audio/opus";
}
- if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Wav)
+ if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Aac)
{
- return "audio/wav";
+ return "audio/aac";
}
if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Flac)
@@ -1021,7 +1021,7 @@ private OpenAIChatMessageContent CreateChatMessageContent(OAIChat.ChatCompletion
return "audio/pcm16";
}
- throw new NotSupportedException($"Unsupported audio output format '{audioOptions.OutputAudioFormat}'. Supported formats are 'wav', 'mp3', 'opus', 'flac' and 'pcm16'.");
+ throw new NotSupportedException($"Unsupported audio output format '{audioOptions.OutputAudioFormat}'. Supported formats are 'wav', 'mp3', 'opus', 'aac', 'flac' and 'pcm16'.");
}
private OpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName)
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs
index d35c29ad6a78..80901b5340ce 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Microsoft.SemanticKernel.Process.Internal;
using Microsoft.SemanticKernel.Process.Models;
@@ -50,5 +51,9 @@ public KernelProcess(KernelProcessState state, IList step
Verify.NotNullOrWhiteSpace(state.Name);
this.Steps = [.. steps];
+ if (threads is not null)
+ {
+ this.Threads = threads.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+ }
}
}
diff --git a/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs b/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs
new file mode 100644
index 000000000000..11bc7431683c
--- /dev/null
+++ b/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.SemanticKernel.Process.UnitTests;
+
+///
+/// Unit tests for .
+///
+public class KernelProcessTests
+{
+ ///
+ /// Verifies that the constructor assigns the supplied
+ /// threads dictionary to the property
+ /// instead of silently discarding it.
+ ///
+ [Fact]
+ public void Constructor_AssignsThreadsParameter_WhenProvided()
+ {
+ // Arrange
+ var state = new KernelProcessState(name: "TestProcess", version: "v1", id: "p1");
+ var steps = new List();
+ var threads = new Dictionary
+ {
+ ["main"] = new KernelProcessAgentThread { ThreadName = "main", ThreadId = "t-1" },
+ ["aux"] = new KernelProcessAgentThread { ThreadName = "aux", ThreadId = "t-2" },
+ };
+
+ // Act
+ var process = new KernelProcess(state, steps, edges: null, threads: threads);
+
+ // Assert
+ Assert.Equal(2, process.Threads.Count);
+ Assert.True(process.Threads.ContainsKey("main"));
+ Assert.True(process.Threads.ContainsKey("aux"));
+ Assert.Equal("t-1", process.Threads["main"].ThreadId);
+ Assert.Equal("t-2", process.Threads["aux"].ThreadId);
+ }
+
+ ///
+ /// Verifies that the constructor leaves
+ /// as an empty dictionary when the
+ /// threads argument is null.
+ ///
+ [Fact]
+ public void Constructor_DefaultsThreadsToEmptyDictionary_WhenNull()
+ {
+ // Arrange
+ var state = new KernelProcessState(name: "TestProcess", version: "v1", id: "p2");
+ var steps = new List();
+
+ // Act
+ var process = new KernelProcess(state, steps, edges: null, threads: null);
+
+ // Assert
+ Assert.NotNull(process.Threads);
+ Assert.Empty(process.Threads);
+ }
+}
diff --git a/dotnet/src/Functions/Functions.OpenApi/CompatibilitySuppressions.xml b/dotnet/src/Functions/Functions.OpenApi/CompatibilitySuppressions.xml
index ff2b43c448eb..cc1b3172c906 100644
--- a/dotnet/src/Functions/Functions.OpenApi/CompatibilitySuppressions.xml
+++ b/dotnet/src/Functions/Functions.OpenApi/CompatibilitySuppressions.xml
@@ -1,52 +1,10 @@
-
- CP0002
- M:Microsoft.SemanticKernel.Plugins.OpenApi.RestApiOperationServerUrlValidationOptions.get_AllowedSchemes
- lib/net10.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- lib/net10.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- true
-
-
- CP0002
- M:Microsoft.SemanticKernel.Plugins.OpenApi.RestApiOperationServerUrlValidationOptions.set_AllowedSchemes(System.Collections.Generic.IReadOnlyList{System.String})
- lib/net10.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- lib/net10.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- true
-
-
- CP0002
- M:Microsoft.SemanticKernel.Plugins.OpenApi.RestApiOperationServerUrlValidationOptions.get_AllowedSchemes
- lib/net8.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- lib/net8.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- true
-
-
- CP0002
- M:Microsoft.SemanticKernel.Plugins.OpenApi.RestApiOperationServerUrlValidationOptions.set_AllowedSchemes(System.Collections.Generic.IReadOnlyList{System.String})
- lib/net8.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- lib/net8.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- true
-
CP0002
F:Microsoft.SemanticKernel.Plugins.OpenApi.OpenApiKernelFunctionContext.KernelFunctionContextKey
lib/netstandard2.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
lib/net8.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
-
- CP0002
- M:Microsoft.SemanticKernel.Plugins.OpenApi.RestApiOperationServerUrlValidationOptions.get_AllowedSchemes
- lib/netstandard2.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- lib/netstandard2.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- true
-
-
- CP0002
- M:Microsoft.SemanticKernel.Plugins.OpenApi.RestApiOperationServerUrlValidationOptions.set_AllowedSchemes(System.Collections.Generic.IReadOnlyList{System.String})
- lib/netstandard2.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- lib/netstandard2.0/Microsoft.SemanticKernel.Plugins.OpenApi.dll
- true
-
\ No newline at end of file
diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/PromptyTests.cs b/dotnet/src/Functions/Functions.Prompty.UnitTests/PromptyTests.cs
index 98732f0e6541..fbe261d70a54 100644
--- a/dotnet/src/Functions/Functions.Prompty.UnitTests/PromptyTests.cs
+++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/PromptyTests.cs
@@ -97,7 +97,6 @@ public void ChatPromptyShouldSupportCreatingOpenAIExecutionSettingsWithJsonObjec
Assert.Equal(0, executionSettings.Temperature);
Assert.Equal(1.0, executionSettings.TopP);
Assert.Null(executionSettings.StopSequences);
- Assert.Equal("{\"type\":\"json_object\"}", executionSettings.ResponseFormat?.ToString());
Assert.Null(executionSettings.TokenSelectionBiases);
Assert.Equal(3000, executionSettings.MaxTokens);
Assert.Null(executionSettings.Seed);
@@ -226,25 +225,41 @@ public void ItFailsToParseAnEmptyHeader()
---
Abc
""")]
- [InlineData("""
- ---a
- name: SomePrompt
- ---
- Abc
- """)]
[InlineData("""
---
name: SomePrompt
---b
Abc
""")]
- public void ItRequiresStringSeparatorPlacement(string prompt)
+ public void ItToleratesLenientFrontmatterSeparatorPlacement(string prompt)
{
// Arrange
Kernel kernel = new();
- // Act / Assert
- Assert.Throws(() => kernel.CreateFunctionFromPrompty(prompt));
+ // Act - Prompty 2.0 tolerates surrounding whitespace and trailing characters on the
+ // frontmatter separators, so these templates are parsed rather than rejected. This is
+ // not a security-relevant relaxation, so no stricter validation is imposed on top.
+ var kernelFunction = kernel.CreateFunctionFromPrompty(prompt);
+
+ // Assert
+ Assert.NotNull(kernelFunction);
+ Assert.Equal("SomePrompt", kernelFunction.Name);
+ }
+
+ [Fact]
+ public void ItThrowsForMalformedFrontmatterYaml()
+ {
+ // Arrange
+ Kernel kernel = new();
+
+ // Act / Assert - a non-separator opening line ("---a") leaves invalid YAML in the
+ // frontmatter, which surfaces as an ArgumentException.
+ Assert.Throws(() => kernel.CreateFunctionFromPrompty("""
+ ---a
+ name: SomePrompt
+ ---
+ Abc
+ """));
}
[Fact]
@@ -342,7 +357,8 @@ public void ItCreatesInputVariablesOnlyWhenNoneAreExplicitlySet()
---
name: MyPrompt
inputs:
- question:
+ - name: question
+ kind: string
description: What is the color of the sky?
---
{{a}} {{b}} {{c}}
@@ -366,20 +382,14 @@ public void ItShouldLoadExecutionSettings()
name: SomePrompt
description: This is the description.
model:
- api: chat
- connection:
- type: azure_openai_beta
+ apiType: chat
options:
- logprobs: true
- top_logprobs: 2
- top_p: 1.0
- user: Bob
- stop_sequences:
+ temperature: 0.5
+ topP: 1.0
+ maxOutputTokens: 1000
+ stopSequences:
- END
- COMPLETE
- token_selection_biases:
- 1: 2
- 3: 4
---
Abc---def
""";
@@ -393,12 +403,10 @@ public void ItShouldLoadExecutionSettings()
Assert.NotNull(executionSettings);
var openaiExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings);
Assert.NotNull(openaiExecutionSettings);
- Assert.True(openaiExecutionSettings.Logprobs);
- Assert.Equal(2, openaiExecutionSettings.TopLogprobs);
+ Assert.Equal(0.5, openaiExecutionSettings.Temperature);
Assert.Equal(1.0, openaiExecutionSettings.TopP);
- Assert.Equal("Bob", openaiExecutionSettings.User);
+ Assert.Equal(1000, openaiExecutionSettings.MaxTokens);
Assert.Equal(["END", "COMPLETE"], openaiExecutionSettings.StopSequences);
- Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, openaiExecutionSettings.TokenSelectionBiases);
}
[Fact]
@@ -442,32 +450,6 @@ public void ItShouldCreateFunctionFromPromptYamlContainingRelativeFileReferences
Assert.Equal("gpt-35-turbo", defaultExecutionSetting.ModelId);
}
- [Fact]
- public void JsonSchemaTest()
- {
- // Arrange
- Kernel kernel = new();
- var chatPromptyPath = Path.Combine("TestData", "chat.prompty");
- var promptyTemplate = File.ReadAllText(chatPromptyPath);
-
- // Act
- var kernelFunction = kernel.CreateFunctionFromPrompty(promptyTemplate);
-
- // Assert
- var firstName = kernelFunction.Metadata.Parameters.First(p => p.Name == "firstName");
- Assert.NotNull(firstName);
- Assert.NotNull(firstName.Schema);
- Assert.Equal("{\"type\":\"string\"}", firstName.Schema.ToString());
- var answer = kernelFunction.Metadata.Parameters.First(p => p.Name == "answer");
- Assert.NotNull(answer);
- Assert.NotNull(answer.Schema);
- Assert.Equal("{\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"},\"citations\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"format\":\"uri\"}}},\"required\":[\"answer\",\"citations\"],\"additionalProperties\":false}", answer.Schema.ToString());
- var other = kernelFunction.Metadata.Parameters.First(p => p.Name == "other");
- Assert.NotNull(other);
- Assert.NotNull(other.Schema);
- Assert.Equal("{\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"},\"citations\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"format\":\"uri\"}}},\"required\":[\"answer\",\"citations\"],\"additionalProperties\":\"false\"}", other.Schema.ToString());
- }
-
private sealed class EchoTextGenerationService : ITextGenerationService
{
public IReadOnlyDictionary Attributes { get; } = new Dictionary();
diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chat.prompty b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chat.prompty
index fd8d177b36bd..d319a91251e4 100644
--- a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chat.prompty
+++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chat.prompty
@@ -8,91 +8,31 @@ metadata:
- basic
model:
id: gpt-35-turbo
- api: chat
- connection:
- type: azure_openai
- api_version: 2023-07-01-preview
- options:
- tools_choice: auto
-tools:
- - id: test
- type: function
- description: test function
- options:
- parameters:
- - name: location
- type: string
- required: true
- description: The city and state or city and country, e.g. San Francisco, CA or Tokyo, Japan
+ apiType: chat
inputs:
- firstName:
- type: string
+ - name: firstName
+ kind: string
default: User
- sample: April
description: The first name of the customer
- json_schema:
- type: string
- lastName:
- type: string
- sample: Kwong
+ - name: lastName
+ kind: string
required: true
- strict: false
description: The last name of the customer
- json_schema:
- type: string
- question:
- type: string
- description: The question to answer
+ - name: question
+ kind: string
required: true
- json_schema:
- type: string
- answer:
- type: object
- description: Some count
+ description: The question to answer
+ - name: answer
+ kind: object
required: true
description: The answer with citations
- json_schema: |
- {
- "type": "object",
- "properties": {
- "answer": {
- "type": "string"
- },
- "citations": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- }
- },
- "required": [
- "answer",
- "citations"
- ],
- "additionalProperties": false
- }
- other:
- type: object
+ - name: other
+ kind: object
required: true
- description: Property with JSON schema
- json_schema:
- type: object
- properties:
- answer:
- type: string
- citations:
- type: array
- items:
- type: string
- format: uri
- required:
- - answer
- - citations
- additionalProperties: false
+ description: Property with additional context
outputs:
- work:
- type: object
+ - name: work
+ kind: object
description: The thing to output
---
system:
@@ -146,4 +86,4 @@ would go well with the items found above. Be brief and concise and use appropria
{% for item in history %}
{{item.role}}:
{{item.content}}
-{% endfor %}
\ No newline at end of file
+{% endfor %}
diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty
index 1788da6d131f..7a3b4cdcde90 100644
--- a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty
+++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty
@@ -6,18 +6,13 @@ metadata:
- markwallace
tags:
- basic
-model:
+model:
id: gpt-4o
- api: chat
- connection:
- type: azure_openai
- azure_deployment: gpt-4o
+ apiType: chat
options:
temperature: 0.0
- max_tokens: 3000
- top_p: 1.0
- response_format:
- type: json_object
+ maxOutputTokens: 3000
+ topP: 1.0
---
system:
You are a classifier agent that should know classify a problem into Easy/Medium/Hard based on the problem description.
@@ -27,4 +22,4 @@ your response should be in a json format with the following structure:
}
user:
-{{question}}
\ No newline at end of file
+{{question}}
diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatNoExecutionSettings.prompty b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatNoExecutionSettings.prompty
index 843da558bfde..8b6f9d445662 100644
--- a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatNoExecutionSettings.prompty
+++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatNoExecutionSettings.prompty
@@ -1,4 +1,4 @@
----
+---
name: prompty_with_no_execution_setting
description: prompty without execution setting
metadata:
@@ -7,6 +7,8 @@ metadata:
tags:
- basic
inputs:
- prompt: dummy
+ - name: prompt
+ kind: string
+ default: dummy
---
-{{prompt}}
\ No newline at end of file
+{{prompt}}
diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/model.json b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/model.json
index ed0812338084..62c56d324045 100644
--- a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/model.json
+++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/model.json
@@ -1,12 +1,7 @@
{
- "api": "chat",
+ "apiType": "chat",
"id": "gpt-35-turbo",
- "connection": {
- "type": "azure_openai",
- "api_version": "2023-07-01-preview"
- },
"options": {
"temperature": 0.5
}
}
-
diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/relativeFileReference.prompty b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/relativeFileReference.prompty
index 7e821ba601a7..d9c095400560 100644
--- a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/relativeFileReference.prompty
+++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/relativeFileReference.prompty
@@ -1,4 +1,4 @@
----
+---
name: TestRelativeFileReference
description: A test prompt for relative file references
metadata:
diff --git a/dotnet/src/Functions/Functions.Prompty/Functions.Prompty.csproj b/dotnet/src/Functions/Functions.Prompty/Functions.Prompty.csproj
index 9fe3545d5b58..f970bc459bda 100644
--- a/dotnet/src/Functions/Functions.Prompty/Functions.Prompty.csproj
+++ b/dotnet/src/Functions/Functions.Prompty/Functions.Prompty.csproj
@@ -3,7 +3,7 @@
Microsoft.SemanticKernel.Prompty
$(AssemblyName)
- net10.0;net8.0;netstandard2.0
+ net10.0
beta
$(NoWarn);CA1812
@@ -19,8 +19,6 @@
-
-
diff --git a/dotnet/src/Functions/Functions.Prompty/KernelFunctionPrompty.cs b/dotnet/src/Functions/Functions.Prompty/KernelFunctionPrompty.cs
index 0ef20e0cedce..9c283f1e2722 100644
--- a/dotnet/src/Functions/Functions.Prompty/KernelFunctionPrompty.cs
+++ b/dotnet/src/Functions/Functions.Prompty/KernelFunctionPrompty.cs
@@ -2,8 +2,7 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text.Json;
+using System.IO;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;
using Microsoft.SemanticKernel.PromptTemplates.Liquid;
@@ -55,24 +54,24 @@ public static PromptTemplateConfig ToPromptTemplateConfig(string promptyTemplate
{
Verify.NotNullOrWhiteSpace(promptyTemplate);
- Dictionary globalConfig = [];
- PromptyCore.Prompty prompty = PromptyCore.Prompty.Load(promptyTemplate, globalConfig, promptyFilePath);
+ PromptyCore.Prompty prompty = LoadPrompty(promptyTemplate, promptyFilePath);
var promptTemplateConfig = new PromptTemplateConfig
{
Name = prompty.Name,
Description = prompty.Description,
- Template = prompty.Content.ToString() ?? string.Empty,
+ Template = prompty.Instructions ?? string.Empty,
};
PromptExecutionSettings? defaultExecutionSetting = null;
- if (prompty.Model?.Id is not null || prompty.Model?.Connection?.ServiceId is not null || prompty.Model?.Options?.Count > 0)
+ var extensionData = ToExtensionData(prompty.Model?.Options);
+ var modelId = prompty.Model?.Id;
+ if (!string.IsNullOrWhiteSpace(modelId) || extensionData.Count > 0)
{
defaultExecutionSetting = new PromptExecutionSettings()
{
- ModelId = prompty.Model.Id,
- ServiceId = prompty.Model.Connection?.ServiceId,
- ExtensionData = prompty.Model.Options,
+ ModelId = string.IsNullOrWhiteSpace(modelId) ? null : modelId,
+ ExtensionData = extensionData.Count > 0 ? extensionData : null,
};
promptTemplateConfig.AddExecutionSettings(defaultExecutionSetting);
}
@@ -80,17 +79,14 @@ public static PromptTemplateConfig ToPromptTemplateConfig(string promptyTemplate
// Add input and output variables.
if (prompty.Inputs is not null)
{
- foreach (var kvp in prompty.Inputs)
+ foreach (var input in prompty.Inputs)
{
- var input = kvp.Value;
promptTemplateConfig.InputVariables.Add(new()
{
- Name = kvp.Key,
+ Name = input.Name,
Default = input.Default,
- IsRequired = input.Required,
- Description = input.Description,
- AllowDangerouslySetContent = !input.Strict,
- JsonSchema = ToJsonSchema(input.JsonSchema),
+ IsRequired = input.Required ?? false,
+ Description = input.Description ?? string.Empty,
});
}
}
@@ -100,35 +96,79 @@ public static PromptTemplateConfig ToPromptTemplateConfig(string promptyTemplate
// contains one and only one, use it. Otherwise, ignore any outputs.
if (prompty.Outputs.Count == 1)
{
- var output = prompty.Outputs.Values.First();
+ var output = prompty.Outputs[0];
promptTemplateConfig.OutputVariable = new()
{
- Description = output.Description,
- JsonSchema = ToJsonSchema(output.JsonSchema),
+ Description = output.Description ?? string.Empty,
};
}
}
// Update template format. If not provided, use Liquid as default.
- promptTemplateConfig.TemplateFormat = prompty.Template?.Format ?? LiquidPromptTemplateFactory.LiquidTemplateFormat;
+ promptTemplateConfig.TemplateFormat =
+ string.IsNullOrEmpty(prompty.Template?.Format?.Kind)
+ ? LiquidPromptTemplateFactory.LiquidTemplateFormat
+ : prompty.Template!.Format!.Kind;
return promptTemplateConfig;
}
#region private
- private static string? ToJsonSchema(object? input)
+ ///
+ /// Loads a from the provided template text, optionally using a file path
+ /// so that ${file:...} references resolve securely within the prompty file's directory.
+ ///
+ private static PromptyCore.Prompty LoadPrompty(string promptyTemplate, string? promptyFilePath)
{
- if (input is null)
+ try
{
- return null;
+ // When a real file path is available, use the file loader so that file references
+ // (${file:...}) are resolved securely, confined to the directory containing the file.
+ if (!string.IsNullOrEmpty(promptyFilePath) && File.Exists(promptyFilePath))
+ {
+ var fullPath = Path.GetFullPath(promptyFilePath);
+ var loadOptions = new PromptyCore.PromptyLoadOptions
+ {
+ AllowedFileRoots = [Path.GetDirectoryName(fullPath)!],
+ };
+
+ return PromptyCore.PromptyLoader.Load(fullPath, loadOptions);
+ }
+
+ // Otherwise parse the frontmatter (and markdown body) directly from the provided text.
+ var frontmatter = PromptyCore.FrontmatterParser.Parse(promptyTemplate);
+ return PromptyCore.Prompty.Load(frontmatter, new PromptyCore.LoadContext());
}
+ catch (Exception ex) when (ex is not ArgumentException)
+ {
+ // Normalize parse/format failures (for example, invalid YAML in the frontmatter) to
+ // ArgumentException to preserve the plugin's input-validation contract.
+ throw new ArgumentException($"Invalid prompty template: {ex.Message}", nameof(promptyTemplate), ex);
+ }
+ }
- if (input is string str)
+ ///
+ /// Converts the strongly typed to the loosely typed extension data
+ /// expected by consumers (for example, the OpenAI connector).
+ /// Only the strongly typed options are mapped; arbitrary provider-specific options are not forwarded.
+ ///
+ private static Dictionary ToExtensionData(PromptyCore.ModelOptions? options)
+ {
+ Dictionary extensionData = [];
+ if (options is null)
{
- return str;
+ return extensionData;
}
- return JsonSerializer.Serialize(input);
+ if (options.Temperature is not null) { extensionData["temperature"] = options.Temperature; }
+ if (options.TopP is not null) { extensionData["top_p"] = options.TopP; }
+ if (options.PresencePenalty is not null) { extensionData["presence_penalty"] = options.PresencePenalty; }
+ if (options.FrequencyPenalty is not null) { extensionData["frequency_penalty"] = options.FrequencyPenalty; }
+ if (options.MaxOutputTokens is not null) { extensionData["max_tokens"] = options.MaxOutputTokens; }
+ if (options.Seed is not null) { extensionData["seed"] = options.Seed; }
+ if (options.StopSequences is { Count: > 0 }) { extensionData["stop_sequences"] = options.StopSequences; }
+
+ return extensionData;
}
#endregion
}
diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj
index 7ed606ec711d..4e739bf9a96a 100644
--- a/dotnet/src/IntegrationTests/IntegrationTests.csproj
+++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj
@@ -56,6 +56,7 @@
+
diff --git a/dotnet/src/IntegrationTests/Plugins/ContextualFunctionProviderTests.cs b/dotnet/src/IntegrationTests/Plugins/ContextualFunctionProviderTests.cs
index d28fc150aeaf..3bdc20dc3e52 100644
--- a/dotnet/src/IntegrationTests/Plugins/ContextualFunctionProviderTests.cs
+++ b/dotnet/src/IntegrationTests/Plugins/ContextualFunctionProviderTests.cs
@@ -7,12 +7,12 @@
using System.Threading.Tasks;
using Azure.AI.OpenAI;
using Azure.Identity;
+using CommunityToolkit.VectorData.InMemory;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
-using Microsoft.SemanticKernel.Connectors.InMemory;
using Microsoft.SemanticKernel.Functions;
using SemanticKernel.IntegrationTests.TestSettings;
using Xunit;
diff --git a/dotnet/src/InternalUtilities/src/Http/HttpClientExtensions.cs b/dotnet/src/InternalUtilities/src/Http/HttpClientExtensions.cs
index 638a0026bb96..48299fefbb13 100644
--- a/dotnet/src/InternalUtilities/src/Http/HttpClientExtensions.cs
+++ b/dotnet/src/InternalUtilities/src/Http/HttpClientExtensions.cs
@@ -29,7 +29,7 @@ internal static async Task SendWithSuccessCheckAsync(this H
HttpResponseMessage? response = null;
try
{
- response = await client.SendAsync(request, completionOption, cancellationToken).ConfigureAwait(false);
+ response = await client.SendAsync(request, completionOption, cancellationToken).ConfigureAwait(false); // CodeQL [SM03781] Internal utility method; URI validation is the caller's responsibility upstream
}
catch (HttpRequestException e)
{
diff --git a/dotnet/src/InternalUtilities/src/RestrictedInternalUtilities.props b/dotnet/src/InternalUtilities/src/RestrictedInternalUtilities.props
index fddc4409da7c..ecbf07ef859d 100644
--- a/dotnet/src/InternalUtilities/src/RestrictedInternalUtilities.props
+++ b/dotnet/src/InternalUtilities/src/RestrictedInternalUtilities.props
@@ -5,6 +5,8 @@
-->
+
+
diff --git a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
index c67b8a9ad089..5d11b4b023e7 100644
--- a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
+++ b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
@@ -113,7 +113,7 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath)
Verify.NotNullOrWhiteSpace(path);
canonicalPath = string.Empty;
- if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase))
+ if (IsUncOrExtendedPath(path))
{
throw new ArgumentException("Invalid file path, UNC paths are not supported.", nameof(path));
}
@@ -125,7 +125,17 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath)
throw new ArgumentException("Invalid file path, a fully qualified file location must be specified.", nameof(path));
}
- canonicalPath = PathUtilities.GetSafeFullPath(path);
+ // Resolve the full path first (a pure string operation that does not touch the
+ // filesystem). A relative path can still resolve to a UNC path here, for example
+ // when the current directory is a UNC share, so re-check before GetSafeFullPath
+ // probes the filesystem while resolving symbolic links.
+ canonicalPath = Path.GetFullPath(path);
+ if (IsUncOrExtendedPath(canonicalPath))
+ {
+ throw new ArgumentException("Invalid file path, UNC paths are not supported.", nameof(path));
+ }
+
+ canonicalPath = PathUtilities.GetSafeFullPath(canonicalPath);
if (File.Exists(canonicalPath) && File.GetAttributes(canonicalPath).HasFlag(FileAttributes.ReadOnly))
{
@@ -162,5 +172,10 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath)
return false;
}
+
+ private static bool IsUncOrExtendedPath(string path) =>
+ path.Length >= 2 &&
+ (path[0] is '/' or '\\') &&
+ (path[1] is '/' or '\\');
#endregion
}
diff --git a/dotnet/src/Plugins/Plugins.Core/Plugins.Core.csproj b/dotnet/src/Plugins/Plugins.Core/Plugins.Core.csproj
index 6ff6cb2f7884..9b0f06f623da 100644
--- a/dotnet/src/Plugins/Plugins.Core/Plugins.Core.csproj
+++ b/dotnet/src/Plugins/Plugins.Core/Plugins.Core.csproj
@@ -25,4 +25,8 @@
+
+
+
+
diff --git a/dotnet/src/Plugins/Plugins.Core/TimePlugin.cs b/dotnet/src/Plugins/Plugins.Core/TimePlugin.cs
index 94b46ad8d296..6a44547576f5 100644
--- a/dotnet/src/Plugins/Plugins.Core/TimePlugin.cs
+++ b/dotnet/src/Plugins/Plugins.Core/TimePlugin.cs
@@ -14,6 +14,17 @@ namespace Microsoft.SemanticKernel.Plugins.Core;
///
public sealed class TimePlugin
{
+ private readonly TimeProvider _timeProvider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The time provider to use. Defaults to .
+ public TimePlugin(TimeProvider? timeProvider = null)
+ {
+ this._timeProvider = timeProvider ?? TimeProvider.System;
+ }
+
///
/// Get the current date
///
@@ -24,7 +35,7 @@ public sealed class TimePlugin
[KernelFunction, Description("Get the current date")]
public string Date(IFormatProvider? formatProvider = null) =>
// Example: Sunday, 12 January, 2025
- DateTimeOffset.Now.ToString("D", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("D", formatProvider);
///
/// Get the current date
@@ -48,7 +59,7 @@ public string Today(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current date and time in the local time zone")]
public string Now(IFormatProvider? formatProvider = null) =>
// Sunday, January 12, 2025 9:15 PM
- DateTimeOffset.Now.ToString("f", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("f", formatProvider);
///
/// Get the current UTC date and time
@@ -60,7 +71,7 @@ public string Now(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current UTC date and time")]
public string UtcNow(IFormatProvider? formatProvider = null) =>
// Sunday, January 13, 2025 5:15 AM
- DateTimeOffset.UtcNow.ToString("f", formatProvider);
+ this._timeProvider.GetUtcNow().ToString("f", formatProvider);
///
/// Get the current time
@@ -72,7 +83,7 @@ public string UtcNow(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current time")]
public string Time(IFormatProvider? formatProvider = null) =>
// Example: 09:15:07 PM
- DateTimeOffset.Now.ToString("hh:mm:ss tt", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("hh:mm:ss tt", formatProvider);
///
/// Get the current year
@@ -84,7 +95,7 @@ public string Time(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current year")]
public string Year(IFormatProvider? formatProvider = null) =>
// Example: 2025
- DateTimeOffset.Now.ToString("yyyy", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("yyyy", formatProvider);
///
/// Get the current month name
@@ -96,7 +107,7 @@ public string Year(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current month name")]
public string Month(IFormatProvider? formatProvider = null) =>
// Example: January
- DateTimeOffset.Now.ToString("MMMM", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("MMMM", formatProvider);
///
/// Get the current month number
@@ -108,7 +119,7 @@ public string Month(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current month number")]
public string MonthNumber(IFormatProvider? formatProvider = null) =>
// Example: 01
- DateTimeOffset.Now.ToString("MM", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("MM", formatProvider);
///
/// Get the current day of the month
@@ -120,7 +131,7 @@ public string MonthNumber(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current day of the month")]
public string Day(IFormatProvider? formatProvider = null) =>
// Example: 12
- DateTimeOffset.Now.ToString("dd", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("dd", formatProvider);
///
/// Get the date a provided number of days in the past
@@ -129,7 +140,7 @@ public string Day(IFormatProvider? formatProvider = null) =>
[KernelFunction]
[Description("Get the date offset by a provided number of days from today")]
public string DaysAgo([Description("The number of days to offset from today")] double input, IFormatProvider? formatProvider = null) =>
- DateTimeOffset.Now.AddDays(-input).ToString("D", formatProvider);
+ this._timeProvider.GetLocalNow().AddDays(-input).ToString("D", formatProvider);
///
/// Get the current day of the week
@@ -141,7 +152,7 @@ public string DaysAgo([Description("The number of days to offset from today")] d
[KernelFunction, Description("Get the current day of the week")]
public string DayOfWeek(IFormatProvider? formatProvider = null) =>
// Example: Sunday
- DateTimeOffset.Now.ToString("dddd", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("dddd", formatProvider);
///
/// Get the current clock hour
@@ -153,7 +164,7 @@ public string DayOfWeek(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current clock hour")]
public string Hour(IFormatProvider? formatProvider = null) =>
// Example: 9 PM
- DateTimeOffset.Now.ToString("h tt", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("h tt", formatProvider);
///
/// Get the current clock 24-hour number
@@ -165,7 +176,7 @@ public string Hour(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current clock 24-hour number")]
public string HourNumber(IFormatProvider? formatProvider = null) =>
// Example: 21
- DateTimeOffset.Now.ToString("HH", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("HH", formatProvider);
///
/// Get the date of the previous day matching the supplied day name
@@ -181,7 +192,7 @@ public string DateMatchingLastDayName(
[Description("The day name to match")] DayOfWeek input,
IFormatProvider? formatProvider = null)
{
- DateTimeOffset dateTime = DateTimeOffset.Now;
+ DateTimeOffset dateTime = this._timeProvider.GetLocalNow();
// Walk backwards from the previous day for up to a week to find the matching day
for (int i = 1; i <= 7; ++i)
@@ -206,7 +217,7 @@ public string DateMatchingLastDayName(
[KernelFunction, Description("Get the minutes on the current hour")]
public string Minute(IFormatProvider? formatProvider = null) =>
// Example: 15
- DateTimeOffset.Now.ToString("mm", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("mm", formatProvider);
///
/// Get the seconds on the current minute
@@ -218,7 +229,7 @@ public string Minute(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the seconds on the current minute")]
public string Second(IFormatProvider? formatProvider = null) =>
// Example: 07
- DateTimeOffset.Now.ToString("ss", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("ss", formatProvider);
///
/// Get the local time zone offset from UTC
@@ -230,7 +241,7 @@ public string Second(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the local time zone offset from UTC")]
public string TimeZoneOffset(IFormatProvider? formatProvider = null) =>
// Example: -08:00
- DateTimeOffset.Now.ToString("%K", formatProvider);
+ this._timeProvider.GetLocalNow().ToString("%K", formatProvider);
///
/// Get the local time zone name
@@ -246,5 +257,5 @@ public string TimeZoneOffset(IFormatProvider? formatProvider = null) =>
public string TimeZoneName() =>
// Example: PST
// Note: this is the "current" timezone and it can change over the year, e.g. from PST to PDT
- TimeZoneInfo.Local.DisplayName;
+ this._timeProvider.LocalTimeZone.DisplayName;
}
diff --git a/dotnet/src/Plugins/Plugins.StructuredData.EntityFramework/Plugins.StructuredData.EntityFramework.csproj b/dotnet/src/Plugins/Plugins.StructuredData.EntityFramework/Plugins.StructuredData.EntityFramework.csproj
index 1a18b0ddc2fc..b84a41e7e72c 100644
--- a/dotnet/src/Plugins/Plugins.StructuredData.EntityFramework/Plugins.StructuredData.EntityFramework.csproj
+++ b/dotnet/src/Plugins/Plugins.StructuredData.EntityFramework/Plugins.StructuredData.EntityFramework.csproj
@@ -44,6 +44,8 @@
+
+
diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
index 29d5ba6c5d1d..0b309f1dfb0d 100644
--- a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
+++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
@@ -144,6 +144,41 @@ public async Task ItCannotReadFromDisallowedFoldersAsync()
await Assert.ThrowsAsync(async () => await plugin.ReadAsync(Path.Combine("", Path.GetRandomFileName())));
}
+ [Theory]
+ [InlineData("\\\\UNC\\server\\folder\\myfile.txt")]
+ [InlineData("//UNC/server/folder/myfile.txt")]
+ [InlineData("/\\UNC\\server\\folder\\myfile.txt")]
+ [InlineData("\\/UNC/server/folder/myfile.txt")]
+ public async Task ItRejectsUncOrExtendedPathsOnReadAsync(string path)
+ {
+ // Arrange
+ var plugin = new FileIOPlugin()
+ {
+ AllowedFolders = [Path.GetTempPath()]
+ };
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() => plugin.ReadAsync(path));
+ }
+
+ [Theory]
+ [InlineData("\\\\UNC\\server\\folder\\myfile.txt")]
+ [InlineData("//UNC/server/folder/myfile.txt")]
+ [InlineData("/\\UNC\\server\\folder\\myfile.txt")]
+ [InlineData("\\/UNC/server/folder/myfile.txt")]
+ public async Task ItRejectsUncOrExtendedPathsOnWriteAsync(string path)
+ {
+ // Arrange
+ var plugin = new FileIOPlugin()
+ {
+ AllowedFolders = [Path.GetTempPath()],
+ DisableFileOverwrite = false
+ };
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() => plugin.WriteAsync(path, "hello world"));
+ }
+
[Fact]
public async Task ItCannotReadThroughSymlinkOutsideAllowedFoldersAsync()
{
diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs
index 3df8f6d2636e..536309d14d02 100644
--- a/dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs
+++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs
@@ -11,44 +11,157 @@
namespace SemanticKernel.Plugins.UnitTests.Core;
-// TODO: allow clock injection and test all functions
public class TimePluginTests
{
+ // Sunday, 15 June 2025 21:15:07 UTC — local timezone pinned to UTC so tests are machine-independent
+ private static readonly DateTimeOffset s_fixedTime = new(2025, 6, 15, 21, 15, 7, TimeSpan.Zero);
+
+ private static TimePlugin CreatePlugin() => new(new FixedUtcTimeProvider(s_fixedTime));
+
+ /// Minimal TimeProvider that returns a fixed UTC instant with UTC as local timezone.
+ private sealed class FixedUtcTimeProvider(DateTimeOffset fixedUtc) : TimeProvider
+ {
+ public override DateTimeOffset GetUtcNow() => fixedUtc.ToUniversalTime();
+ public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
+ }
+
[Fact]
public void ItCanBeInstantiated()
{
- // Act - Assert no exception occurs
var _ = new TimePlugin();
}
[Fact]
public void ItCanBeImported()
{
- // Act - Assert no exception occurs e.g. due to reflection
Assert.NotNull(KernelPluginFactory.CreateFromType("time"));
}
[Fact]
- public void DaysAgo()
+ public void Date()
+ {
+ // InvariantCulture "D" format: dddd, dd MMMM yyyy
+ Assert.Equal("Sunday, 15 June 2025", CreatePlugin().Date(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Today()
{
- double interval = 2;
- DateTime expected = DateTime.Now.AddDays(-interval);
- var plugin = new TimePlugin();
- string result = plugin.DaysAgo(interval, CultureInfo.CurrentCulture);
- DateTime returned = DateTime.Parse(result, CultureInfo.CurrentCulture);
- Assert.Equal(expected.Day, returned.Day);
- Assert.Equal(expected.Month, returned.Month);
- Assert.Equal(expected.Year, returned.Year);
+ Assert.Equal("Sunday, 15 June 2025", CreatePlugin().Today(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Now()
+ {
+ // InvariantCulture "f" format: dddd, dd MMMM yyyy HH:mm
+ Assert.Equal("Sunday, 15 June 2025 21:15", CreatePlugin().Now(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void UtcNow()
+ {
+ Assert.Equal("Sunday, 15 June 2025 21:15", CreatePlugin().UtcNow(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Time()
+ {
+ Assert.Equal("09:15:07 PM", CreatePlugin().Time(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Year()
+ {
+ Assert.Equal("2025", CreatePlugin().Year(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Month()
+ {
+ Assert.Equal("June", CreatePlugin().Month(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void MonthNumber()
+ {
+ Assert.Equal("06", CreatePlugin().MonthNumber(CultureInfo.InvariantCulture));
}
[Fact]
public void Day()
{
- string expected = DateTime.Now.ToString("dd", CultureInfo.CurrentCulture);
- var plugin = new TimePlugin();
- string result = plugin.Day(CultureInfo.CurrentCulture);
- Assert.Equal(expected, result);
- Assert.True(int.TryParse(result, out _));
+ Assert.Equal("15", CreatePlugin().Day(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void DayOfWeek()
+ {
+ Assert.Equal("Sunday", CreatePlugin().DayOfWeek(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Hour()
+ {
+ Assert.Equal("9 PM", CreatePlugin().Hour(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void HourNumber()
+ {
+ Assert.Equal("21", CreatePlugin().HourNumber(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Minute()
+ {
+ Assert.Equal("15", CreatePlugin().Minute(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Second()
+ {
+ Assert.Equal("07", CreatePlugin().Second(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void TimeZoneOffset()
+ {
+ Assert.Equal("+00:00", CreatePlugin().TimeZoneOffset(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void TimeZoneName()
+ {
+ // FixedUtcTimeProvider pins LocalTimeZone to TimeZoneInfo.Utc
+ Assert.Equal(TimeZoneInfo.Utc.DisplayName, CreatePlugin().TimeZoneName());
+ }
+
+ [Fact]
+ public void DaysAgo()
+ {
+ // 2 days before 2025-06-15 is 2025-06-13 (Friday)
+ Assert.Equal("Friday, 13 June 2025", CreatePlugin().DaysAgo(2, CultureInfo.InvariantCulture));
+ }
+
+ [Theory]
+ [MemberData(nameof(DayOfWeekCases))]
+ public void DateMatchingLastDayName(DayOfWeek dayName, string expectedDate)
+ {
+ // Fixed time is Sunday 2025-06-15; walk back to find each day
+ Assert.Equal(expectedDate, CreatePlugin().DateMatchingLastDayName(dayName, CultureInfo.InvariantCulture));
+ }
+
+ public static IEnumerable