diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 871f35361f2e..fbc8eb2d8ff4 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -8,7 +8,7 @@
-
+
@@ -106,8 +106,8 @@
-
-
+
+
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/VectorData/SqliteVec/SqliteVec.csproj b/dotnet/src/VectorData/SqliteVec/SqliteVec.csproj
index ab5a3f4798dd..e5c9e680965f 100644
--- a/dotnet/src/VectorData/SqliteVec/SqliteVec.csproj
+++ b/dotnet/src/VectorData/SqliteVec/SqliteVec.csproj
@@ -32,6 +32,7 @@
+