mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Add support for values params for functions (#65)
### Motivation and Context
1. When writing a prompt one might want to use the symbols `{{` and `}}`
without triggering the template engine.
2. When invoking functions, one might want to pass a value rather than a
variable, e.g. `{{ recall "my preferred browser" }}`.
3. Semantic functions might want to call other semantic functions.
### Description
This PR adds support for "values", so special syntax can be embedded,
and so that functions can be invoked with predefined values without the
need to create a temporary variable.
E.g.
```
{{ "{{" }} and {{ "}}" }} are special SK sequences.
```
```
... {{ read 'c:\documents\ai\file.txt' }} ...
```
```
... {{ "quotes' \"escaping\" example" }} ...
```
```
... {{ 'quotes\' "escaping" example' }} ...
```
```
{{ 'two special chars \\\' here' }}
```
Added missing tests for all the template language, including end to end
integration tests covering several edge cases.
Also: semantic functions can now call other semantic functions. The
previous implementation would allow semantic functions to call only
native functions.
This commit is contained in:
@@ -38,6 +38,10 @@ you can write:
|
||||
|
||||
This will produce a greeting with the user's name.
|
||||
|
||||
Spaces are ignored, so if you find it more readable, you can also write:
|
||||
|
||||
Hello {{ $name }}, welcome to Semantic Kernel!
|
||||
|
||||
## Function calls
|
||||
|
||||
To call an external function and embed the result in your text, use the
|
||||
@@ -57,26 +61,31 @@ For instance, the code above is equivalent to:
|
||||
## Function parameters
|
||||
|
||||
To call an external function and pass a parameter to it, use the
|
||||
`{namespace.functionName $varName}` syntax.
|
||||
`{{namespace.functionName $varName}}` and
|
||||
`{{namespace.functionName "value"}}` syntax.
|
||||
For example, if you want to pass a different input to the weather forecast
|
||||
function, you can write:
|
||||
|
||||
The weather today in {{$city}} is {weather.getForecast $city}.
|
||||
The weather today in {{$region}} is {weather.getForecast $region}.
|
||||
The weather today in {{$city}} is {{weather.getForecast $city}}.
|
||||
The weather today in Schio is {{weather.getForecast "Schio"}}.
|
||||
|
||||
This will produce two sentences with the weather forecast for two different
|
||||
locations, using the city stored in the `city` variable and the region name
|
||||
stored in the `region` variable.
|
||||
locations, using the city stored in the _`city`_ **variable** and the _"Schio"_
|
||||
location **value** hardcoded in the prompt template.
|
||||
|
||||
## Design Principles
|
||||
|
||||
The template language uses of the `$` symbol on purpose, to clearly distinguish
|
||||
between variables, which are retrieved from local temporary memory, and
|
||||
functions that retrieve content executing some code.
|
||||
The template language is designed to be simple and fast to render, allowing
|
||||
to create functions with a simple text editor, reducing special syntax to a
|
||||
minimum, and minimizing edge cases.
|
||||
|
||||
Branching features such as "if", "for", and code blocks are not part of SK's
|
||||
The template language uses the **«`$`»** symbol on purpose, to clearly distinguish
|
||||
between function calls that retrieve content executing some code, from variables,
|
||||
which are replaced with data from the local temporary memory.
|
||||
|
||||
Branching features such as _"if"_, _"for"_, and code blocks are not part of SK's
|
||||
template language. This reflects SK's design principle of using natural language
|
||||
as much as possible, with a clear separation from conventional programming code.
|
||||
as much as possible, with a clear separation from traditional programming code.
|
||||
|
||||
By using a simple language, the kernel can also avoid complex parsing and
|
||||
external dependencies, resulting in a fast and memory efficient processing.
|
||||
@@ -91,6 +100,7 @@ template, using the syntax described.
|
||||
```
|
||||
My name: {{msgraph.GetMyName}}
|
||||
My email: {{msgraph.GetMyEmailAddress}}
|
||||
My hobbies: {{memory.recall "my hobbies"}}
|
||||
Recipient: {{$recipient}}
|
||||
Email to reply to:
|
||||
=========
|
||||
@@ -132,3 +142,112 @@ async Task<string> GenResponseToEmailAsync(
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Notes about special chars
|
||||
|
||||
Semantic function templates are text files, so there is no need to escape special chars
|
||||
like new lines and tabs. However, there are two cases that require a special syntax:
|
||||
|
||||
1. Including double curly braces in the prompt templates
|
||||
2. Passing to functions hardcoded values that include quotes
|
||||
|
||||
## Prompts needing double curly braces
|
||||
|
||||
Double curly braces have a special use case, they are used to inject variables,
|
||||
values, and functions into templates.
|
||||
|
||||
If you need to include the **`{{`** and **`}}`** sequences in your prompts, which
|
||||
could trigger special rendering logic, the best solution is to use string values
|
||||
enclosed in quotes, like `{{ "{{" }}` and `{{ "}}" }}`
|
||||
|
||||
For example:
|
||||
|
||||
{{ "{{" }} and {{ "}}" }} are special SK sequences.
|
||||
|
||||
will render to:
|
||||
|
||||
{{ and }} are special SK sequences.
|
||||
|
||||
## Values that include quotes, and escaping
|
||||
|
||||
Values can be enclosed using **single quotes** and **double quotes**.
|
||||
|
||||
To avoid the need for special syntax, when working with a value that contains
|
||||
_single quotes_, we recommend wrapping the value with _double quotes_. Similarly,
|
||||
when using a value that contains _double quotes_, wrap the value with _single quotes_.
|
||||
|
||||
For example:
|
||||
|
||||
...text... {{ functionName "one 'quoted' word" }} ...text...
|
||||
...text... {{ functionName 'one "quoted" word' }} ...text...
|
||||
|
||||
For those cases where the value contains both single and double quotes, you will
|
||||
need _escaping_, using the special **«`\`»** symbol.
|
||||
|
||||
When using double quotes around a value, use **«`\"`»** to include a double quote
|
||||
symbol inside the value:
|
||||
|
||||
... {{ "quotes' \"escaping\" example" }} ...
|
||||
|
||||
and similarly, when using single quotes, use **«`\'`»** to include a single quote
|
||||
inside the value:
|
||||
|
||||
... {{ 'quotes\' "escaping" example' }} ...
|
||||
|
||||
Both are rendered to:
|
||||
|
||||
... quotes' "escaping" example ...
|
||||
|
||||
Note that for consistency, the sequences **«`\'`»** and **«`\"`»** do always render
|
||||
to **«`'`»** and **«`"`»**, even when escaping might not be required.
|
||||
|
||||
For instance:
|
||||
|
||||
... {{ 'no need to \"escape" ' }} ...
|
||||
|
||||
is equivalent to:
|
||||
|
||||
... {{ 'no need to "escape" ' }} ...
|
||||
|
||||
and both render to:
|
||||
|
||||
... no need to "escape" ...
|
||||
|
||||
In case you may need to render a backslash in front of a quote, since **«`\`»**
|
||||
is a special char, you will need to escape it too, and use the special sequences
|
||||
**«`\\\'`»** and **«`\\\"`»**.
|
||||
|
||||
For example:
|
||||
|
||||
{{ 'two special chars \\\' here' }}
|
||||
|
||||
is rendered to:
|
||||
|
||||
two special chars \' here
|
||||
|
||||
Similarly to single and double quotes, the symbol **«`\`»** doesn't always need
|
||||
to be escaped. However, for consistency, it can be escaped even when not required.
|
||||
|
||||
For instance:
|
||||
|
||||
... {{ 'c:\\documents\\ai' }} ...
|
||||
|
||||
is equivalent to:
|
||||
|
||||
... {{ 'c:\documents\ai' }} ...
|
||||
|
||||
and both are rendered to:
|
||||
|
||||
... c:\documents\ai ...
|
||||
|
||||
Lastly, backslashes have a special meaning only when used in front of
|
||||
**«`'`»**, **«`"`»** and **«`\`»**.
|
||||
|
||||
In all other cases, the backslash character has no impact and is rendered as is.
|
||||
For example:
|
||||
|
||||
{{ "nothing special about these sequences: \0 \n \t \r \foo" }}
|
||||
|
||||
is rendered to:
|
||||
|
||||
nothing special about these sequences: \0 \n \t \r \foo
|
||||
|
||||
@@ -47,5 +47,12 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageVersion Include="xunit.analyzers" Version="1.1.0" />
|
||||
<PackageReference Include="xunit.analyzers" />
|
||||
<PackageVersion Include="Moq.Analyzers" Version="0.0.9" />
|
||||
<PackageReference Include="Moq.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -172,6 +172,7 @@ public void It$SOMENAME$()
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DEC4D140B393B04F8A18C0913BDE0592/Shortcut/@EntryValue">copy</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DEC4D140B393B04F8A18C0913BDE0592/Text/@EntryValue">// Copyright (c) Microsoft. All rights reserved.
|
||||
</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=arrivederci/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=awaiters/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=davinci/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Joinable/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
<None Update="testsettings.development.json" Condition="Exists('testsettings.development.json')">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Include="TemplateLanguage\tests.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using Microsoft.SemanticKernel.TemplateEngine;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SemanticKernel.IntegrationTests.TemplateLanguage;
|
||||
|
||||
#pragma warning disable VSTHRD103 // ok to use WriteLine synchronously
|
||||
#pragma warning disable CA1849 // ok to use WriteLine synchronously
|
||||
|
||||
public sealed class PromptTemplateEngineTests : IDisposable
|
||||
{
|
||||
public PromptTemplateEngineTests(ITestOutputHelper output)
|
||||
{
|
||||
this._logger = new RedirectOutput(output);
|
||||
this._target = new PromptTemplateEngine();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItSupportsVariablesAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string input = "template tests";
|
||||
const string winner = "SK";
|
||||
const string template = "And the winner\n of {{$input}} \nis: {{ $winner }}!";
|
||||
|
||||
var kernel = Kernel.Builder.Build();
|
||||
var context = kernel.CreateNewContext();
|
||||
context["input"] = input;
|
||||
context["winner"] = winner;
|
||||
|
||||
// Act
|
||||
var result = await this._target.RenderAsync(template, context);
|
||||
|
||||
// Assert
|
||||
var expected = template
|
||||
.Replace("{{$input}}", input, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{{ $winner }}", winner, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItSupportsValuesAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string template = "And the winner\n of {{'template\ntests'}} \nis: {{ \"SK\" }}!";
|
||||
const string expected = "And the winner\n of template\ntests \nis: SK!";
|
||||
|
||||
var kernel = Kernel.Builder.Build();
|
||||
var context = kernel.CreateNewContext();
|
||||
|
||||
// Act
|
||||
var result = await this._target.RenderAsync(template, context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItAllowsToPassVariablesToFunctionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string template = "== {{my.check123 $call}} ==";
|
||||
var kernel = Kernel.Builder.Build();
|
||||
kernel.ImportSkill(new MySkill(), "my");
|
||||
var context = kernel.CreateNewContext();
|
||||
context["call"] = "123";
|
||||
|
||||
// Act
|
||||
var result = await this._target.RenderAsync(template, context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("== 123 ok ==", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItAllowsToPassValuesToFunctionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string template = "== {{my.check123 '234'}} ==";
|
||||
var kernel = Kernel.Builder.Build();
|
||||
kernel.ImportSkill(new MySkill(), "my");
|
||||
var context = kernel.CreateNewContext();
|
||||
|
||||
// Act
|
||||
var result = await this._target.RenderAsync(template, context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("== 234 != 123 ==", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItAllowsToPassEscapedValues1ToFunctionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const char ESC = '\\';
|
||||
string template = "== {{my.check123 'a" + ESC + "'b'}} ==";
|
||||
var kernel = Kernel.Builder.Build();
|
||||
kernel.ImportSkill(new MySkill(), "my");
|
||||
var context = kernel.CreateNewContext();
|
||||
|
||||
// Act
|
||||
var result = await this._target.RenderAsync(template, context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("== a'b != 123 ==", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItAllowsToPassEscapedValues2ToFunctionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const char ESC = '\\';
|
||||
string template = "== {{my.check123 \"a" + ESC + "\"b\"}} ==";
|
||||
var kernel = Kernel.Builder.Build();
|
||||
kernel.ImportSkill(new MySkill(), "my");
|
||||
var context = kernel.CreateNewContext();
|
||||
|
||||
// Act
|
||||
var result = await this._target.RenderAsync(template, context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("== a\"b != 123 ==", result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetTemplateLanguageTests))]
|
||||
public async Task ItHandleEdgeCasesAsync(string template, string expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var kernel = Kernel.Builder.Build();
|
||||
kernel.ImportSkill(new MySkill());
|
||||
|
||||
// Act
|
||||
this._logger.WriteLine("template: " + template);
|
||||
this._logger.WriteLine("expected: " + expectedResult);
|
||||
if (expectedResult.StartsWith("ERROR", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
await Assert.ThrowsAsync<TemplateException>(
|
||||
async () => await this._target.RenderAsync(template, kernel.CreateNewContext()));
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await this._target.RenderAsync(template, kernel.CreateNewContext());
|
||||
this._logger.WriteLine(" result: " + result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTemplateLanguageTests()
|
||||
{
|
||||
return GetTestData("TemplateLanguage/tests.txt");
|
||||
}
|
||||
|
||||
public class MySkill
|
||||
{
|
||||
[SKFunction("This is a test")]
|
||||
[SKFunctionName("check123")]
|
||||
public string MyFunction(string input)
|
||||
{
|
||||
return input == "123" ? "123 ok" : input + " != 123";
|
||||
}
|
||||
|
||||
[SKFunction("This is a test")]
|
||||
[SKFunctionName("asis")]
|
||||
public string MyFunction2(string input)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
#region internals
|
||||
|
||||
private readonly RedirectOutput _logger;
|
||||
private readonly PromptTemplateEngine _target;
|
||||
|
||||
private static IEnumerable<string[]> GetTestData(string file)
|
||||
{
|
||||
if (!File.Exists(file)) { Assert.Fail("File not found: " + file); }
|
||||
|
||||
var content = File.ReadLines(file);
|
||||
var key = string.Empty;
|
||||
foreach (string value in content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || value.StartsWith('#')) { continue; }
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
key = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new string[] { key, value };
|
||||
key = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._logger.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#pragma warning restore VSTHRD103
|
||||
#pragma warning restore CA1849
|
||||
@@ -0,0 +1,73 @@
|
||||
# About this file:
|
||||
# - The file is used by PromptTemplateEngineTests.cs to test SK template language.
|
||||
# - By using a TXT file there is no ambiguity caused by C#/Python escaping syntax.
|
||||
# - Empty lines and lines starting with "#" are ignored.
|
||||
# - Lines are NOT trimmed, there might be empty spaces at the end on purpose.
|
||||
# - The file contains multiple test cases.
|
||||
# - Each test case consists of two lines:
|
||||
# - line 1: the template to render
|
||||
# - line 2: the expected result after rendering
|
||||
# - If a template is invalid, line 2 contains the value "ERROR", e.g. a TemplateException is expected.
|
||||
|
||||
""
|
||||
""
|
||||
|
||||
{}
|
||||
{}
|
||||
|
||||
{{}}
|
||||
{{}}
|
||||
|
||||
.{{asis}}.
|
||||
..
|
||||
|
||||
a{{asis ''}}b
|
||||
ab
|
||||
|
||||
{{asis 'a'}}
|
||||
a
|
||||
|
||||
{{ asis 'foo' }}
|
||||
foo
|
||||
|
||||
# The second quote means the value is never closed, hiding the closing brackets
|
||||
# turning the entire string as a static text
|
||||
{{ asis 'foo\' }}
|
||||
{{ asis 'foo\' }}
|
||||
|
||||
{{ asis 'f\'11' }}
|
||||
f'11
|
||||
|
||||
{{ asis "f\\\'22" }}
|
||||
f\'22
|
||||
|
||||
# The last quote hides the closing }}
|
||||
{{ call 'f\\'33" }}
|
||||
{{ call 'f\\'33" }}
|
||||
|
||||
# \ is escaped but the second quote is not, terminating the string
|
||||
# After the string is terminated the <x> token is invalid
|
||||
{{ call 'f\\'x }}
|
||||
ERROR
|
||||
|
||||
# \ is escaped but the second quote is not, terminating the string
|
||||
# After the string is terminated the <xy> token is invalid
|
||||
{{ call 'f\\'xy }}
|
||||
ERROR
|
||||
|
||||
{{ "{{" }} and {{ "}}" }} x
|
||||
{{ and }} x
|
||||
|
||||
{{ " nothing special about these sequences: \ \0 \n \t \r \foo" }}
|
||||
nothing special about these sequences: \ \0 \n \t \r \foo
|
||||
|
||||
1{{ '\\' }}2
|
||||
1\2
|
||||
|
||||
# Even number of escaped \
|
||||
{{ "\\\\\\\\" }}
|
||||
\\\\
|
||||
|
||||
# Odd number of escaped \
|
||||
{{ "\\\\\\\\\\" }}
|
||||
\\\\\
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel.AI;
|
||||
using Microsoft.SemanticKernel.Memory;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
@@ -31,7 +33,7 @@ public class CodeBlockTests
|
||||
{
|
||||
// Arrange
|
||||
var context = new SKContext(new ContextVariables(), NullMemory.Instance, this._skills.Object, this._log.Object);
|
||||
this._skills.Setup(x => x.HasNativeFunction("functionName")).Returns(false);
|
||||
this._skills.Setup(x => x.HasFunction("functionName")).Returns(false);
|
||||
var target = new CodeBlock("functionName", this._log.Object);
|
||||
|
||||
// Act
|
||||
@@ -50,8 +52,8 @@ public class CodeBlockTests
|
||||
function
|
||||
.Setup(x => x.InvokeAsync(It.IsAny<SKContext?>(), It.IsAny<CompleteRequestSettings?>(), It.IsAny<ILogger?>(), It.IsAny<CancellationToken?>()))
|
||||
.Throws(new RuntimeWrappedException("error"));
|
||||
this._skills.Setup(x => x.HasNativeFunction("functionName")).Returns(true);
|
||||
this._skills.Setup(x => x.GetNativeFunction("functionName")).Returns(function.Object);
|
||||
this._skills.Setup(x => x.HasFunction("functionName")).Returns(true);
|
||||
this._skills.Setup(x => x.GetFunction("functionName")).Returns(function.Object);
|
||||
var target = new CodeBlock("functionName", this._log.Object);
|
||||
|
||||
// Act
|
||||
@@ -60,4 +62,234 @@ public class CodeBlockTests
|
||||
// Assert
|
||||
Assert.Equal(TemplateException.ErrorCodes.RuntimeError, exception.ErrorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItHasTheCorrectType()
|
||||
{
|
||||
// Act
|
||||
var target = new CodeBlock("", NullLogger.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(BlockTypes.Code, target.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTrimsSpaces()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.Equal("aa", new CodeBlock(" aa ", NullLogger.Instance).Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItChecksValidityOfInternalBlocks()
|
||||
{
|
||||
// Arrange
|
||||
var validBlock1 = new FunctionIdBlock("x");
|
||||
var validBlock2 = new ValBlock("''");
|
||||
var invalidBlock = new VarBlock("");
|
||||
|
||||
// Act
|
||||
var codeBlock1 = new CodeBlock(new List<Block> { validBlock1, validBlock2 }, "", NullLogger.Instance);
|
||||
var codeBlock2 = new CodeBlock(new List<Block> { validBlock1, invalidBlock }, "", NullLogger.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.True(codeBlock1.IsValid(out _));
|
||||
Assert.False(codeBlock2.IsValid(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItRequiresAValidFunctionCall()
|
||||
{
|
||||
// Arrange
|
||||
var funcId = new FunctionIdBlock("funcName");
|
||||
var valBlock = new ValBlock("'value'");
|
||||
var varBlock = new VarBlock("$var");
|
||||
|
||||
// Act
|
||||
var codeBlock1 = new CodeBlock(new List<Block> { funcId, valBlock }, "", NullLogger.Instance);
|
||||
var codeBlock2 = new CodeBlock(new List<Block> { funcId, varBlock }, "", NullLogger.Instance);
|
||||
var codeBlock3 = new CodeBlock(new List<Block> { funcId, funcId }, "", NullLogger.Instance);
|
||||
var codeBlock4 = new CodeBlock(new List<Block> { funcId, varBlock, varBlock }, "", NullLogger.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.True(codeBlock1.IsValid(out _));
|
||||
Assert.True(codeBlock2.IsValid(out _));
|
||||
|
||||
// Assert - Can't pass a function to a function
|
||||
Assert.False(codeBlock3.IsValid(out _));
|
||||
|
||||
// Assert - Can't pass more than one param
|
||||
Assert.False(codeBlock4.IsValid(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItRendersCodeBlockConsistingOfJustAVarBlock1Async()
|
||||
{
|
||||
// Arrange
|
||||
var variables = new ContextVariables { ["varName"] = "foo" };
|
||||
var context = new SKContext(variables, NullMemory.Instance, null, NullLogger.Instance);
|
||||
|
||||
// Act
|
||||
var codeBlock = new CodeBlock("$varName", NullLogger.Instance);
|
||||
var result = await codeBlock.RenderCodeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("foo", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItRendersCodeBlockConsistingOfJustAVarBlock2Async()
|
||||
{
|
||||
// Arrange
|
||||
var variables = new ContextVariables { ["varName"] = "bar" };
|
||||
var context = new SKContext(variables, NullMemory.Instance, null, NullLogger.Instance);
|
||||
var varBlock = new VarBlock("$varName");
|
||||
|
||||
// Act
|
||||
var codeBlock = new CodeBlock(new List<Block> { varBlock }, "", NullLogger.Instance);
|
||||
var result = await codeBlock.RenderCodeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("bar", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItRendersCodeBlockConsistingOfJustAValBlock1Async()
|
||||
{
|
||||
// Arrange
|
||||
var context = new SKContext(new ContextVariables(), NullMemory.Instance, null, NullLogger.Instance);
|
||||
|
||||
// Act
|
||||
var codeBlock = new CodeBlock("'ciao'", NullLogger.Instance);
|
||||
var result = await codeBlock.RenderCodeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("ciao", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItRendersCodeBlockConsistingOfJustAValBlock2Async()
|
||||
{
|
||||
// Arrange
|
||||
var context = new SKContext(new ContextVariables(), NullMemory.Instance, null, NullLogger.Instance);
|
||||
var valBlock = new ValBlock("'arrivederci'");
|
||||
|
||||
// Act
|
||||
var codeBlock = new CodeBlock(new List<Block> { valBlock }, "", NullLogger.Instance);
|
||||
var result = await codeBlock.RenderCodeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("arrivederci", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItInvokesFunctionCloningAllVariablesAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string FUNC = "funcName";
|
||||
|
||||
var variables = new ContextVariables { ["input"] = "zero", ["var1"] = "uno", ["var2"] = "due" };
|
||||
var context = new SKContext(variables, NullMemory.Instance, this._skills.Object, NullLogger.Instance);
|
||||
var funcId = new FunctionIdBlock(FUNC);
|
||||
|
||||
var canary0 = string.Empty;
|
||||
var canary1 = string.Empty;
|
||||
var canary2 = string.Empty;
|
||||
var function = new Mock<ISKFunction>();
|
||||
function
|
||||
.Setup(x => x.InvokeAsync(It.IsAny<SKContext?>(), It.IsAny<CompleteRequestSettings?>(), It.IsAny<ILogger?>(), It.IsAny<CancellationToken?>()))
|
||||
.Callback<SKContext?, CompleteRequestSettings?, ILogger?, CancellationToken?>((ctx, _, _, _) =>
|
||||
{
|
||||
canary0 = ctx!["input"];
|
||||
canary1 = ctx["var1"];
|
||||
canary2 = ctx["var2"];
|
||||
|
||||
ctx["input"] = "overridden";
|
||||
ctx["var1"] = "overridden";
|
||||
ctx["var2"] = "overridden";
|
||||
});
|
||||
|
||||
this._skills.Setup(x => x.HasFunction(FUNC)).Returns(true);
|
||||
this._skills.Setup(x => x.GetFunction(FUNC)).Returns(function.Object);
|
||||
|
||||
// Act
|
||||
var codeBlock = new CodeBlock(new List<Block> { funcId }, "", NullLogger.Instance);
|
||||
string result = await codeBlock.RenderCodeAsync(context);
|
||||
|
||||
// Assert - Values are received
|
||||
Assert.Equal("zero", canary0);
|
||||
Assert.Equal("uno", canary1);
|
||||
Assert.Equal("due", canary2);
|
||||
|
||||
// Assert - Original context is intact
|
||||
Assert.Equal("zero", variables["input"]);
|
||||
Assert.Equal("uno", variables["var1"]);
|
||||
Assert.Equal("due", variables["var2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItInvokesFunctionWithCustomVariableAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string FUNC = "funcName";
|
||||
const string VAR = "varName";
|
||||
const string VAR_VALUE = "varValue";
|
||||
|
||||
var variables = new ContextVariables { [VAR] = VAR_VALUE };
|
||||
var context = new SKContext(variables, NullMemory.Instance, this._skills.Object, NullLogger.Instance);
|
||||
var funcId = new FunctionIdBlock(FUNC);
|
||||
var varBlock = new VarBlock($"${VAR}");
|
||||
|
||||
var canary = string.Empty;
|
||||
var function = new Mock<ISKFunction>();
|
||||
function
|
||||
.Setup(x => x.InvokeAsync(It.IsAny<SKContext?>(), It.IsAny<CompleteRequestSettings?>(), It.IsAny<ILogger?>(), It.IsAny<CancellationToken?>()))
|
||||
.Callback<SKContext?, CompleteRequestSettings?, ILogger?, CancellationToken?>((ctx, _, _, _) =>
|
||||
{
|
||||
canary = ctx!["input"];
|
||||
});
|
||||
|
||||
this._skills.Setup(x => x.HasFunction(FUNC)).Returns(true);
|
||||
this._skills.Setup(x => x.GetFunction(FUNC)).Returns(function.Object);
|
||||
|
||||
// Act
|
||||
var codeBlock = new CodeBlock(new List<Block> { funcId, varBlock }, "", NullLogger.Instance);
|
||||
string result = await codeBlock.RenderCodeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VAR_VALUE, result);
|
||||
Assert.Equal(VAR_VALUE, canary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItInvokesFunctionWithCustomValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string FUNC = "funcName";
|
||||
const string VALUE = "value";
|
||||
|
||||
var context = new SKContext(new ContextVariables(), NullMemory.Instance, this._skills.Object, NullLogger.Instance);
|
||||
var funcId = new FunctionIdBlock(FUNC);
|
||||
var valBlock = new ValBlock($"'{VALUE}'");
|
||||
|
||||
var canary = string.Empty;
|
||||
var function = new Mock<ISKFunction>();
|
||||
function
|
||||
.Setup(x => x.InvokeAsync(It.IsAny<SKContext?>(), It.IsAny<CompleteRequestSettings?>(), It.IsAny<ILogger?>(), It.IsAny<CancellationToken?>()))
|
||||
.Callback<SKContext?, CompleteRequestSettings?, ILogger?, CancellationToken?>((ctx, _, _, _) =>
|
||||
{
|
||||
canary = ctx!["input"];
|
||||
});
|
||||
|
||||
this._skills.Setup(x => x.HasFunction(FUNC)).Returns(true);
|
||||
this._skills.Setup(x => x.GetFunction(FUNC)).Returns(function.Object);
|
||||
|
||||
// Act
|
||||
var codeBlock = new CodeBlock(new List<Block> { funcId, valBlock }, "", NullLogger.Instance);
|
||||
string result = await codeBlock.RenderCodeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VALUE, result);
|
||||
Assert.Equal(VALUE, canary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel.TemplateEngine;
|
||||
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.UnitTests.TemplateEngine.Blocks;
|
||||
|
||||
public class FunctionIdBlockTests
|
||||
{
|
||||
[Fact]
|
||||
public void ItHasTheCorrectType()
|
||||
{
|
||||
// Act
|
||||
var target = new FunctionIdBlock("", NullLogger.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(BlockTypes.FunctionId, target.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTrimsSpaces()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.Equal("aa", new FunctionIdBlock(" aa ", NullLogger.Instance).Content);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0", true)]
|
||||
[InlineData("1", true)]
|
||||
[InlineData("a", true)]
|
||||
[InlineData("_", true)]
|
||||
[InlineData("01", true)]
|
||||
[InlineData("01a", true)]
|
||||
[InlineData("a01", true)]
|
||||
[InlineData("_0", true)]
|
||||
[InlineData("a01_", true)]
|
||||
[InlineData("_a01", true)]
|
||||
[InlineData(".", true)]
|
||||
[InlineData("a.b", true)]
|
||||
[InlineData("-", false)]
|
||||
[InlineData("a b", false)]
|
||||
[InlineData("a\nb", false)]
|
||||
[InlineData("a\tb", false)]
|
||||
[InlineData("a\rb", false)]
|
||||
[InlineData("a,b", false)]
|
||||
[InlineData("a-b", false)]
|
||||
[InlineData("a+b", false)]
|
||||
[InlineData("a~b", false)]
|
||||
[InlineData("a`b", false)]
|
||||
[InlineData("a!b", false)]
|
||||
[InlineData("a@b", false)]
|
||||
[InlineData("a#b", false)]
|
||||
[InlineData("a$b", false)]
|
||||
[InlineData("a%b", false)]
|
||||
[InlineData("a^b", false)]
|
||||
[InlineData("a*b", false)]
|
||||
[InlineData("a(b", false)]
|
||||
[InlineData("a)b", false)]
|
||||
[InlineData("a|b", false)]
|
||||
[InlineData("a{b", false)]
|
||||
[InlineData("a}b", false)]
|
||||
[InlineData("a[b", false)]
|
||||
[InlineData("a]b", false)]
|
||||
[InlineData("a:b", false)]
|
||||
[InlineData("a;b", false)]
|
||||
[InlineData("a'b", false)]
|
||||
[InlineData("a\"b", false)]
|
||||
[InlineData("a<b", false)]
|
||||
[InlineData("a>b", false)]
|
||||
[InlineData("a/b", false)]
|
||||
[InlineData("a\\b", false)]
|
||||
public void ItAllowsUnderscoreDotsLettersAndDigits(string name, bool isValid)
|
||||
{
|
||||
// Arrange
|
||||
var target = new FunctionIdBlock($" {name} ");
|
||||
|
||||
// Act + Assert
|
||||
Assert.Equal(isValid, target.IsValid(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItAllowsOnlyOneDot()
|
||||
{
|
||||
// Arrange
|
||||
var target1 = new FunctionIdBlock("functionName");
|
||||
var target2 = new FunctionIdBlock("skillName.functionName");
|
||||
Assert.Throws<TemplateException>(() => new FunctionIdBlock("foo.skillName.functionName"));
|
||||
|
||||
// Act + Assert
|
||||
Assert.True(target1.IsValid(out _));
|
||||
Assert.True(target2.IsValid(out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.UnitTests.TemplateEngine.Blocks;
|
||||
|
||||
public class TextBlockTests
|
||||
{
|
||||
[Fact]
|
||||
public void ItHasTheCorrectType()
|
||||
{
|
||||
// Act
|
||||
var target = new TextBlock("");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(BlockTypes.Text, target.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItPreservesEmptyValues()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.Equal("", new TextBlock(null).Content);
|
||||
Assert.Equal("", new TextBlock("").Content);
|
||||
Assert.Equal(" ", new TextBlock(" ").Content);
|
||||
Assert.Equal(" ", new TextBlock(" ").Content);
|
||||
Assert.Equal(" \n", new TextBlock(" \n").Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItIsAlwaysValid()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.True(new TextBlock(null).IsValid(out _));
|
||||
Assert.True(new TextBlock("").IsValid(out _));
|
||||
Assert.True(new TextBlock(" ").IsValid(out _));
|
||||
Assert.True(new TextBlock(" ").IsValid(out _));
|
||||
Assert.True(new TextBlock(" \n").IsValid(out _));
|
||||
Assert.True(new TextBlock(" \nabc").IsValid(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItRendersTheContentAsIs()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.Equal("", new TextBlock(null).Render(null));
|
||||
Assert.Equal("", new TextBlock("").Render(null));
|
||||
Assert.Equal(" ", new TextBlock(" ").Render(null));
|
||||
Assert.Equal(" ", new TextBlock(" ").Render(null));
|
||||
Assert.Equal(" \n", new TextBlock(" \n").Render(null));
|
||||
Assert.Equal("'x'", new TextBlock("'x'").Render(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.UnitTests.TemplateEngine.Blocks;
|
||||
|
||||
public class ValBlockTests
|
||||
{
|
||||
[Fact]
|
||||
public void ItHasTheCorrectType()
|
||||
{
|
||||
// Act
|
||||
var target = new ValBlock("");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(BlockTypes.Value, target.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTrimsSpaces()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.Equal("' '", new ValBlock(" ' ' ").Content);
|
||||
Assert.Equal("\" \"", new ValBlock(" \" \" ").Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItChecksIfAValueStartsWithQuote()
|
||||
{
|
||||
// Assert
|
||||
Assert.True(ValBlock.HasValPrefix("'"));
|
||||
Assert.True(ValBlock.HasValPrefix("'a"));
|
||||
Assert.True(ValBlock.HasValPrefix("\""));
|
||||
Assert.True(ValBlock.HasValPrefix("\"b"));
|
||||
|
||||
Assert.False(ValBlock.HasValPrefix("d'"));
|
||||
Assert.False(ValBlock.HasValPrefix("e\""));
|
||||
Assert.False(ValBlock.HasValPrefix(null));
|
||||
Assert.False(ValBlock.HasValPrefix(""));
|
||||
Assert.False(ValBlock.HasValPrefix("v"));
|
||||
Assert.False(ValBlock.HasValPrefix("-"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItRequiresConsistentQuotes()
|
||||
{
|
||||
// Arrange
|
||||
var validBlock1 = new ValBlock("'ciao'");
|
||||
var validBlock2 = new ValBlock("\"hello\"");
|
||||
var badBlock1 = new ValBlock("'nope\"");
|
||||
var badBlock2 = new ValBlock("'no\"");
|
||||
|
||||
// Act + Assert
|
||||
Assert.True(validBlock1.IsValid(out _));
|
||||
Assert.True(validBlock2.IsValid(out _));
|
||||
Assert.False(badBlock1.IsValid(out _));
|
||||
Assert.False(badBlock2.IsValid(out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
using Microsoft.SemanticKernel.TemplateEngine;
|
||||
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.UnitTests.TemplateEngine.Blocks;
|
||||
|
||||
public class VarBlockTests
|
||||
{
|
||||
[Fact]
|
||||
public void ItHasTheCorrectType()
|
||||
{
|
||||
// Act
|
||||
var target = new VarBlock("");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(BlockTypes.Variable, target.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTrimsSpaces()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.Equal("$", new VarBlock(" $ ").Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItIgnoresSpacesAround()
|
||||
{
|
||||
// Act
|
||||
var target = new VarBlock(" $var \n ");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("$var", target.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItRendersToEmptyStringWithoutVariables()
|
||||
{
|
||||
// Arrange
|
||||
var target = new VarBlock(" $var \n ");
|
||||
|
||||
// Act
|
||||
var result = target.Render(null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItRendersToEmptyStringIfVariableIsMissing()
|
||||
{
|
||||
// Arrange
|
||||
var target = new VarBlock(" $var \n ");
|
||||
var variables = new ContextVariables
|
||||
{
|
||||
["foo"] = "bar"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = target.Render(variables);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItRendersToVariableValueWhenAvailable()
|
||||
{
|
||||
// Arrange
|
||||
var target = new VarBlock(" $var \n ");
|
||||
var variables = new ContextVariables
|
||||
{
|
||||
["foo"] = "bar",
|
||||
["var"] = "able",
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = target.Render(variables);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("able", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItThrowsIfTheVarNameIsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var variables = new ContextVariables
|
||||
{
|
||||
["foo"] = "bar",
|
||||
["var"] = "able",
|
||||
};
|
||||
var target = new VarBlock(" $ ");
|
||||
|
||||
// Act + Assert
|
||||
var ex = Assert.Throws<TemplateException>(() => target.Render(variables));
|
||||
Assert.Equal(TemplateException.ErrorCodes.SyntaxError, ex.ErrorCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0", true)]
|
||||
[InlineData("1", true)]
|
||||
[InlineData("a", true)]
|
||||
[InlineData("_", true)]
|
||||
[InlineData("01", true)]
|
||||
[InlineData("01a", true)]
|
||||
[InlineData("a01", true)]
|
||||
[InlineData("_0", true)]
|
||||
[InlineData("a01_", true)]
|
||||
[InlineData("_a01", true)]
|
||||
[InlineData(".", false)]
|
||||
[InlineData("-", false)]
|
||||
[InlineData("a b", false)]
|
||||
[InlineData("a\nb", false)]
|
||||
[InlineData("a\tb", false)]
|
||||
[InlineData("a\rb", false)]
|
||||
[InlineData("a.b", false)]
|
||||
[InlineData("a,b", false)]
|
||||
[InlineData("a-b", false)]
|
||||
[InlineData("a+b", false)]
|
||||
[InlineData("a~b", false)]
|
||||
[InlineData("a`b", false)]
|
||||
[InlineData("a!b", false)]
|
||||
[InlineData("a@b", false)]
|
||||
[InlineData("a#b", false)]
|
||||
[InlineData("a$b", false)]
|
||||
[InlineData("a%b", false)]
|
||||
[InlineData("a^b", false)]
|
||||
[InlineData("a*b", false)]
|
||||
[InlineData("a(b", false)]
|
||||
[InlineData("a)b", false)]
|
||||
[InlineData("a|b", false)]
|
||||
[InlineData("a{b", false)]
|
||||
[InlineData("a}b", false)]
|
||||
[InlineData("a[b", false)]
|
||||
[InlineData("a]b", false)]
|
||||
[InlineData("a:b", false)]
|
||||
[InlineData("a;b", false)]
|
||||
[InlineData("a'b", false)]
|
||||
[InlineData("a\"b", false)]
|
||||
[InlineData("a<b", false)]
|
||||
[InlineData("a>b", false)]
|
||||
[InlineData("a/b", false)]
|
||||
[InlineData("a\\b", false)]
|
||||
public void ItAllowsUnderscoreLettersAndDigits(string name, bool isValid)
|
||||
{
|
||||
// Arrange
|
||||
var target = new VarBlock($" ${name} ");
|
||||
var variables = new ContextVariables { [name] = "value" };
|
||||
|
||||
// Act
|
||||
var result = target.Render(variables);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(isValid, target.IsValid(out _));
|
||||
if (isValid) { Assert.Equal("value", result); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel.TemplateEngine;
|
||||
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.UnitTests.TemplateEngine;
|
||||
|
||||
public class CodeTokenizerTests
|
||||
{
|
||||
private readonly CodeTokenizer _target;
|
||||
|
||||
public CodeTokenizerTests()
|
||||
{
|
||||
this._target = new CodeTokenizer();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItParsesEmptyText()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.Empty(this._target.Tokenize(null));
|
||||
Assert.Empty(this._target.Tokenize(""));
|
||||
Assert.Empty(this._target.Tokenize(" "));
|
||||
Assert.Empty(this._target.Tokenize(" \n "));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("$", "$")]
|
||||
[InlineData(" $ ", "$")]
|
||||
[InlineData("$foo", "$foo")]
|
||||
[InlineData("$foo ", "$foo")]
|
||||
[InlineData(" $foo", "$foo")]
|
||||
[InlineData(" $bar ", "$bar")]
|
||||
public void ItParsesVarBlocks(string template, string content)
|
||||
{
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Single(blocks);
|
||||
Assert.Equal(content, blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.Variable, blocks[0].Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("'", "'")]
|
||||
[InlineData(" \" ", "\"")]
|
||||
[InlineData("'foo'", "'foo'")]
|
||||
[InlineData("'foo' ", "'foo'")]
|
||||
[InlineData(" 'foo'", "'foo'")]
|
||||
[InlineData(" \"bar\" ", "\"bar\"")]
|
||||
public void ItParsesValBlocks(string template, string content)
|
||||
{
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Single(blocks);
|
||||
Assert.Equal(content, blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.Value, blocks[0].Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("f", "f")]
|
||||
[InlineData(" x ", "x")]
|
||||
[InlineData("foo", "foo")]
|
||||
[InlineData("fo.o ", "fo.o")]
|
||||
[InlineData(" f.oo", "f.oo")]
|
||||
[InlineData(" bar ", "bar")]
|
||||
public void ItParsesFunctionIdBlocks(string template, string content)
|
||||
{
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Single(blocks);
|
||||
Assert.Equal(content, blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.FunctionId, blocks[0].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItParsesFunctionCalls()
|
||||
{
|
||||
// Arrange
|
||||
var template1 = "x.y $foo";
|
||||
var template2 = "xy $foo";
|
||||
var template3 = "xy '$value'";
|
||||
|
||||
// Act
|
||||
var blocks1 = this._target.Tokenize(template1);
|
||||
var blocks2 = this._target.Tokenize(template2);
|
||||
var blocks3 = this._target.Tokenize(template3);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, blocks1.Count);
|
||||
Assert.Equal(2, blocks2.Count);
|
||||
Assert.Equal(2, blocks3.Count);
|
||||
|
||||
Assert.Equal("x.y", blocks1[0].Content);
|
||||
Assert.Equal("xy", blocks2[0].Content);
|
||||
Assert.Equal("xy", blocks3[0].Content);
|
||||
|
||||
Assert.Equal(BlockTypes.FunctionId, blocks1[0].Type);
|
||||
Assert.Equal(BlockTypes.FunctionId, blocks2[0].Type);
|
||||
Assert.Equal(BlockTypes.FunctionId, blocks3[0].Type);
|
||||
|
||||
Assert.Equal("$foo", blocks1[1].Content);
|
||||
Assert.Equal("$foo", blocks2[1].Content);
|
||||
Assert.Equal("'$value'", blocks3[1].Content);
|
||||
|
||||
Assert.Equal(BlockTypes.Variable, blocks1[1].Type);
|
||||
Assert.Equal(BlockTypes.Variable, blocks2[1].Type);
|
||||
Assert.Equal(BlockTypes.Value, blocks3[1].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItSupportsEscaping()
|
||||
{
|
||||
// Arrange
|
||||
var template = "func 'f\\'oo'";
|
||||
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, blocks.Count);
|
||||
Assert.Equal("func", blocks[0].Content);
|
||||
Assert.Equal("'f\'oo'", blocks[1].Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItThrowsWhenSeparatorsAreMissing()
|
||||
{
|
||||
// Arrange
|
||||
var template1 = @"call 'f\\'xy'";
|
||||
var template2 = @"call 'f\\'x";
|
||||
|
||||
// Act
|
||||
Assert.Throws<TemplateException>(() => this._target.Tokenize(template1));
|
||||
Assert.Throws<TemplateException>(() => this._target.Tokenize(template2));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel.Memory;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
@@ -12,134 +11,28 @@ using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
using Moq;
|
||||
using SemanticKernel.UnitTests.XunitHelpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SemanticKernel.UnitTests.TemplateEngine;
|
||||
|
||||
#pragma warning disable VSTHRD103 // ok to use WriteLine synchronously
|
||||
#pragma warning disable CA1849 // ok to use WriteLine synchronously
|
||||
|
||||
public sealed class PromptTemplateEngineTests
|
||||
{
|
||||
private readonly IPromptTemplateEngine _target;
|
||||
private readonly ContextVariables _variables;
|
||||
private readonly Mock<IReadOnlySkillCollection> _skills;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ITestOutputHelper _logger;
|
||||
|
||||
public PromptTemplateEngineTests()
|
||||
public PromptTemplateEngineTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
this._logger = ConsoleLogger.Log;
|
||||
this._target = new PromptTemplateEngine(this._logger);
|
||||
this._logger = testOutputHelper;
|
||||
this._target = new PromptTemplateEngine(ConsoleLogger.Log);
|
||||
this._variables = new ContextVariables(Guid.NewGuid().ToString("X"));
|
||||
this._skills = new Mock<IReadOnlySkillCollection>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTokenizesEdgeCases1()
|
||||
{
|
||||
// Arrange
|
||||
var template = "}}{{{ {$a}}}} {{b}}x}}";
|
||||
|
||||
// Act
|
||||
var blocks = this._target.ExtractBlocks(template, false);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, blocks.Count);
|
||||
|
||||
Assert.Equal("}}{", blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[0].Type);
|
||||
|
||||
Assert.Equal("{$a", blocks[1].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[1].Type);
|
||||
|
||||
Assert.Equal("}} ", blocks[2].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[2].Type);
|
||||
|
||||
Assert.Equal("b", blocks[3].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[3].Type);
|
||||
|
||||
Assert.Equal("x}}", blocks[4].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[4].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTokenizesEdgeCases2()
|
||||
{
|
||||
// Arrange
|
||||
var template = "}}{{{{$a}}}} {{b}}$x}}";
|
||||
|
||||
// Act
|
||||
var blocks = this._target.ExtractBlocks(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, blocks.Count);
|
||||
|
||||
Assert.Equal("}}{{", blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[0].Type);
|
||||
|
||||
Assert.Equal("$a", blocks[1].Content);
|
||||
Assert.Equal(BlockTypes.Variable, blocks[1].Type);
|
||||
|
||||
Assert.Equal("}} ", blocks[2].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[2].Type);
|
||||
|
||||
Assert.Equal("b", blocks[3].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[3].Type);
|
||||
|
||||
Assert.Equal("$x}}", blocks[4].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[4].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTokenizesAClassicPrompt()
|
||||
{
|
||||
// Arrange
|
||||
var template = "this is a {{ $prompt }} with {{$some}} variables " +
|
||||
"and {{function $calls}} that {{ also $use $variables }}";
|
||||
|
||||
// Act
|
||||
var blocks = this._target.ExtractBlocks(template, true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(8, blocks.Count);
|
||||
|
||||
Assert.Equal("this is a ", blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[0].Type);
|
||||
|
||||
Assert.Equal("$prompt", blocks[1].Content);
|
||||
Assert.Equal(BlockTypes.Variable, blocks[1].Type);
|
||||
|
||||
Assert.Equal(" with ", blocks[2].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[2].Type);
|
||||
|
||||
Assert.Equal("$some", blocks[3].Content);
|
||||
Assert.Equal(BlockTypes.Variable, blocks[3].Type);
|
||||
|
||||
Assert.Equal(" variables and ", blocks[4].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[4].Type);
|
||||
|
||||
Assert.Equal("function $calls", blocks[5].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[5].Type);
|
||||
|
||||
Assert.Equal(" that ", blocks[6].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[6].Type);
|
||||
|
||||
Assert.Equal("also $use $variables", blocks[7].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[7].Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 1)]
|
||||
[InlineData("", 1)]
|
||||
[InlineData("}}{{a}} {{b}}x", 5)]
|
||||
[InlineData("}}{{ -a}} {{b}}x", 5)]
|
||||
[InlineData("}}{{ -a\n}} {{b}}x", 5)]
|
||||
[InlineData("}}{{ -a\n} } {{b}}x", 3)]
|
||||
public void ItTokenizesTheRightTokenCount(string? template, int blockCount)
|
||||
{
|
||||
// Act
|
||||
var blocks = this._target.ExtractBlocks(template, false);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(blockCount, blocks.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItRendersVariables()
|
||||
{
|
||||
@@ -230,24 +123,24 @@ public sealed class PromptTemplateEngineTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItRendersCodeUsingInputInstanceAsync()
|
||||
public async Task ItRendersCodeUsingInputAsync()
|
||||
{
|
||||
// Arrange
|
||||
[SKFunction("test")]
|
||||
[SKFunctionName("test")]
|
||||
string MyFunctionAsync(SKContext cx)
|
||||
{
|
||||
this._logger.LogTrace("MyFunction call received, input: {0}", cx.Variables.Input);
|
||||
this._logger.WriteLine("MyFunction call received, input: {0}", cx.Variables.Input);
|
||||
return $"F({cx.Variables.Input})";
|
||||
}
|
||||
|
||||
var func = SKFunction.FromNativeMethod(Method(MyFunctionAsync), this);
|
||||
ISKFunction? func = SKFunction.FromNativeMethod(Method(MyFunctionAsync), this);
|
||||
Assert.NotNull(func);
|
||||
|
||||
this._variables.Update("INPUT-BAR");
|
||||
var template = "foo-{{function}}-baz";
|
||||
this._skills.Setup(x => x.HasNativeFunction("function")).Returns(true);
|
||||
this._skills.Setup(x => x.GetNativeFunction("function")).Returns(func);
|
||||
this._skills.Setup(x => x.HasFunction("function")).Returns(true);
|
||||
this._skills.Setup(x => x.GetFunction("function")).Returns(func);
|
||||
var context = this.MockContext();
|
||||
|
||||
// Act
|
||||
@@ -258,80 +151,24 @@ public sealed class PromptTemplateEngineTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItRendersCodeUsingInputStaticAsync()
|
||||
{
|
||||
// Arrange
|
||||
[SKFunction("test")]
|
||||
[SKFunctionName("test")]
|
||||
static string MyFunctionAsync(SKContext cx)
|
||||
{
|
||||
ConsoleLogger.Log.LogTrace("MyFunction call received, input: {0}", cx.Variables.Input);
|
||||
return $"F({cx.Variables.Input})";
|
||||
}
|
||||
|
||||
var func = SKFunction.FromNativeMethod(Method(MyFunctionAsync));
|
||||
Assert.NotNull(func);
|
||||
|
||||
this._variables.Update("INPUT-BAR");
|
||||
var template = "foo-{{function}}-baz";
|
||||
this._skills.Setup(x => x.HasNativeFunction("function")).Returns(true);
|
||||
this._skills.Setup(x => x.GetNativeFunction("function")).Returns(func);
|
||||
var context = this.MockContext();
|
||||
|
||||
// Act
|
||||
var result = await this._target.RenderAsync(template, context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("foo-F(INPUT-BAR)-baz", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItRendersCodeUsingVariablesInstanceAsync()
|
||||
public async Task ItRendersCodeUsingVariablesAsync()
|
||||
{
|
||||
// Arrange
|
||||
[SKFunction("test")]
|
||||
[SKFunctionName("test")]
|
||||
string MyFunctionAsync(SKContext cx)
|
||||
{
|
||||
this._logger.LogTrace("MyFunction call received, input: {0}", cx.Variables.Input);
|
||||
this._logger.WriteLine("MyFunction call received, input: {0}", cx.Variables.Input);
|
||||
return $"F({cx.Variables.Input})";
|
||||
}
|
||||
|
||||
var func = SKFunction.FromNativeMethod(Method(MyFunctionAsync), this);
|
||||
ISKFunction? func = SKFunction.FromNativeMethod(Method(MyFunctionAsync), this);
|
||||
Assert.NotNull(func);
|
||||
|
||||
this._variables.Set("myVar", "BAR");
|
||||
var template = "foo-{{function $myVar}}-baz";
|
||||
this._skills.Setup(x => x.HasNativeFunction("function")).Returns(true);
|
||||
this._skills.Setup(x => x.GetNativeFunction("function")).Returns(func);
|
||||
var context = this.MockContext();
|
||||
|
||||
// Act
|
||||
var result = await this._target.RenderAsync(template, context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("foo-F(BAR)-baz", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItRendersCodeUsingVariablesStaticAsync()
|
||||
{
|
||||
// Arrange
|
||||
[SKFunction("test")]
|
||||
[SKFunctionName("test")]
|
||||
static string MyFunctionAsync(SKContext cx)
|
||||
{
|
||||
ConsoleLogger.Log.LogTrace("MyFunction call received, input: {0}", cx.Variables.Input);
|
||||
return $"F({cx.Variables.Input})";
|
||||
}
|
||||
|
||||
var func = SKFunction.FromNativeMethod(Method(MyFunctionAsync));
|
||||
Assert.NotNull(func);
|
||||
|
||||
this._variables.Set("myVar", "BAR");
|
||||
var template = "foo-{{function $myVar}}-baz";
|
||||
this._skills.Setup(x => x.HasNativeFunction("function")).Returns(true);
|
||||
this._skills.Setup(x => x.GetNativeFunction("function")).Returns(func);
|
||||
this._skills.Setup(x => x.HasFunction("function")).Returns(true);
|
||||
this._skills.Setup(x => x.GetFunction("function")).Returns(func);
|
||||
var context = this.MockContext();
|
||||
|
||||
// Act
|
||||
@@ -350,17 +187,18 @@ public sealed class PromptTemplateEngineTests
|
||||
Task<string> MyFunctionAsync(SKContext cx)
|
||||
{
|
||||
// Input value should be "BAR" because the variable $myVar is passed in
|
||||
this._logger.LogTrace("MyFunction call received, input: {0}", cx.Variables.Input);
|
||||
this._logger.WriteLine("MyFunction call received, input: {0}", cx.Variables.Input);
|
||||
return Task.FromResult(cx.Variables.Input);
|
||||
}
|
||||
|
||||
var func = SKFunction.FromNativeMethod(Method(MyFunctionAsync), this);
|
||||
ISKFunction? func = SKFunction.FromNativeMethod(Method(MyFunctionAsync), this);
|
||||
Assert.NotNull(func);
|
||||
|
||||
this._variables.Set("myVar", "BAR");
|
||||
|
||||
var template = "foo-{{function $myVar}}-baz";
|
||||
this._skills.Setup(x => x.HasNativeFunction("function")).Returns(true);
|
||||
this._skills.Setup(x => x.GetNativeFunction("function")).Returns(func);
|
||||
this._skills.Setup(x => x.HasFunction("function")).Returns(true);
|
||||
this._skills.Setup(x => x.GetFunction("function")).Returns(func);
|
||||
var context = this.MockContext();
|
||||
|
||||
// Act
|
||||
@@ -381,6 +219,8 @@ public sealed class PromptTemplateEngineTests
|
||||
this._variables,
|
||||
NullMemory.Instance,
|
||||
this._skills.Object,
|
||||
this._logger);
|
||||
ConsoleLogger.Log);
|
||||
}
|
||||
}
|
||||
#pragma warning restore VSTHRD103
|
||||
#pragma warning restore CA1849
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel.TemplateEngine;
|
||||
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.UnitTests.TemplateEngine;
|
||||
|
||||
public class TemplateTokenizerTests
|
||||
{
|
||||
private readonly TemplateTokenizer _target;
|
||||
|
||||
public TemplateTokenizerTests()
|
||||
{
|
||||
this._target = new TemplateTokenizer();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, BlockTypes.Text)]
|
||||
[InlineData("", BlockTypes.Text)]
|
||||
[InlineData(" ", BlockTypes.Text)]
|
||||
[InlineData(" ", BlockTypes.Text)]
|
||||
[InlineData(" {} ", BlockTypes.Text)]
|
||||
[InlineData(" {{} ", BlockTypes.Text)]
|
||||
[InlineData(" {{ } } } ", BlockTypes.Text)]
|
||||
[InlineData(" { { }} }", BlockTypes.Text)]
|
||||
[InlineData("{{}}", BlockTypes.Text)]
|
||||
[InlineData("{{ }}", BlockTypes.Text)]
|
||||
[InlineData("{{ }}", BlockTypes.Text)]
|
||||
[InlineData("{{ '}}x", BlockTypes.Text)]
|
||||
[InlineData("{{ \"}}x", BlockTypes.Text)]
|
||||
internal void ItParsesTextWithoutCode(string? text, BlockTypes type)
|
||||
{
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(text);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, blocks.Count);
|
||||
Assert.Equal(type, blocks[0].Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", BlockTypes.Text)]
|
||||
[InlineData(" ", BlockTypes.Text)]
|
||||
[InlineData(" ", BlockTypes.Text)]
|
||||
[InlineData(" aaa ", BlockTypes.Text)]
|
||||
[InlineData("{{$}}", BlockTypes.Variable)]
|
||||
[InlineData("{{$a}}", BlockTypes.Variable)]
|
||||
[InlineData("{{ $a}}", BlockTypes.Variable)]
|
||||
[InlineData("{{ $a }}", BlockTypes.Variable)]
|
||||
[InlineData("{{ $a }}", BlockTypes.Variable)]
|
||||
[InlineData("{{code}}", BlockTypes.Code)]
|
||||
[InlineData("{{code }}", BlockTypes.Code)]
|
||||
[InlineData("{{ code }}", BlockTypes.Code)]
|
||||
[InlineData("{{ code }}", BlockTypes.Code)]
|
||||
[InlineData("{{ code }}", BlockTypes.Code)]
|
||||
[InlineData("{{''}}", BlockTypes.Value)]
|
||||
[InlineData("{{' '}}", BlockTypes.Value)]
|
||||
[InlineData("{{ ' '}}", BlockTypes.Value)]
|
||||
[InlineData("{{ ' ' }}", BlockTypes.Value)]
|
||||
[InlineData("{{ ' ' }}", BlockTypes.Value)]
|
||||
[InlineData("{{ ' ' }}", BlockTypes.Value)]
|
||||
internal void ItParsesBasicBlocks(string? text, BlockTypes type)
|
||||
{
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(text);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, blocks.Count);
|
||||
Assert.Equal(type, blocks[0].Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 1)]
|
||||
[InlineData("", 1)]
|
||||
[InlineData("}}{{a}} {{b}}x", 5)]
|
||||
[InlineData("}}{{ -a}} {{b}}x", 5)]
|
||||
[InlineData("}}{{ -a\n}} {{b}}x", 5)]
|
||||
[InlineData("}}{{ -a\n} } {{b}}x", 3)]
|
||||
public void ItTokenizesTheRightTokenCount(string? template, int blockCount)
|
||||
{
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(blockCount, blocks.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTokenizesEdgeCasesCorrectly1()
|
||||
{
|
||||
// Act
|
||||
var blocks1 = this._target.Tokenize("{{{{a}}");
|
||||
var blocks2 = this._target.Tokenize("{{'{{a}}");
|
||||
var blocks3 = this._target.Tokenize("{{'a}}");
|
||||
var blocks4 = this._target.Tokenize("{{a'}}");
|
||||
|
||||
// Assert - Count
|
||||
Assert.Equal(2, blocks1.Count);
|
||||
Assert.Equal(1, blocks2.Count);
|
||||
Assert.Equal(1, blocks3.Count);
|
||||
Assert.Equal(1, blocks4.Count);
|
||||
|
||||
// Assert - Type
|
||||
Assert.Equal(BlockTypes.Text, blocks1[0].Type);
|
||||
Assert.Equal(BlockTypes.Code, blocks1[1].Type);
|
||||
|
||||
// Assert - Content
|
||||
Assert.Equal("{{", blocks1[0].Content);
|
||||
Assert.Equal("a", blocks1[1].Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTokenizesEdgeCasesCorrectly2()
|
||||
{
|
||||
// Arrange
|
||||
var template = "}}{{{ {$a}}}} {{b}}x}}";
|
||||
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, blocks.Count);
|
||||
|
||||
Assert.Equal("}}{", blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[0].Type);
|
||||
|
||||
Assert.Equal("{$a", blocks[1].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[1].Type);
|
||||
|
||||
Assert.Equal("}} ", blocks[2].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[2].Type);
|
||||
|
||||
Assert.Equal("b", blocks[3].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[3].Type);
|
||||
|
||||
Assert.Equal("x}}", blocks[4].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[4].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTokenizesEdgeCasesCorrectly3()
|
||||
{
|
||||
// Arrange
|
||||
var template = "}}{{{{$a}}}} {{b}}$x}}";
|
||||
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, blocks.Count);
|
||||
|
||||
Assert.Equal("}}{{", blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[0].Type);
|
||||
|
||||
Assert.Equal("$a", blocks[1].Content);
|
||||
Assert.Equal(BlockTypes.Variable, blocks[1].Type);
|
||||
|
||||
Assert.Equal("}} ", blocks[2].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[2].Type);
|
||||
|
||||
Assert.Equal("b", blocks[3].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[3].Type);
|
||||
|
||||
Assert.Equal("$x}}", blocks[4].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[4].Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{{a$}}")]
|
||||
[InlineData("{{a$a}}")]
|
||||
[InlineData("{{a''}}")]
|
||||
[InlineData("{{a\"\"}}")]
|
||||
[InlineData("{{a'b'}}")]
|
||||
[InlineData("{{a\"b\"}}")]
|
||||
[InlineData("{{a'b' }}")]
|
||||
[InlineData("{{a\"b\" }}")]
|
||||
[InlineData("{{ asis 'f\\'oo' }}")]
|
||||
public void ItTokenizesEdgeCasesCorrectly4(string template)
|
||||
{
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, blocks.Count);
|
||||
Assert.Equal(BlockTypes.Code, blocks[0].Type);
|
||||
Assert.Equal(template[2..^2].Trim(), blocks[0].Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItTokenizesATypicalPrompt()
|
||||
{
|
||||
// Arrange
|
||||
var template = "this is a {{ $prompt }} with {{$some}} variables " +
|
||||
"and {{function $calls}} {{ and 'values' }}";
|
||||
|
||||
// Act
|
||||
var blocks = this._target.Tokenize(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(8, blocks.Count);
|
||||
|
||||
Assert.Equal("this is a ", blocks[0].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[0].Type);
|
||||
|
||||
Assert.Equal("$prompt", blocks[1].Content);
|
||||
Assert.Equal(BlockTypes.Variable, blocks[1].Type);
|
||||
|
||||
Assert.Equal(" with ", blocks[2].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[2].Type);
|
||||
|
||||
Assert.Equal("$some", blocks[3].Content);
|
||||
Assert.Equal(BlockTypes.Variable, blocks[3].Type);
|
||||
|
||||
Assert.Equal(" variables and ", blocks[4].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[4].Type);
|
||||
|
||||
Assert.Equal("function $calls", blocks[5].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[5].Type);
|
||||
|
||||
Assert.Equal(" ", blocks[6].Content);
|
||||
Assert.Equal(BlockTypes.Text, blocks[6].Type);
|
||||
|
||||
Assert.Equal("and 'values'", blocks[7].Content);
|
||||
Assert.Equal(BlockTypes.Code, blocks[7].Type);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ internal class MinHeap<T> : IEnumerable<T> where T : IComparable<T>
|
||||
|
||||
this._items = new T[capacity + 1];
|
||||
//
|
||||
// The 0'th item is a sentinal entry that simplies the code
|
||||
// The 0'th item is a sentinel entry that simplifies the code
|
||||
//
|
||||
this._items[0] = minValue;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ internal class MinHeap<T> : IEnumerable<T> where T : IComparable<T>
|
||||
}
|
||||
}
|
||||
|
||||
public int Capacity => this._items.Length - 1; // 0'th item is always a sentinal to simplify code
|
||||
public int Capacity => this._items.Length - 1; // 0'th item is always a sentinel to simplify code
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
@@ -87,7 +87,7 @@ internal class MinHeap<T> : IEnumerable<T> where T : IComparable<T>
|
||||
public void Add(T item)
|
||||
{
|
||||
//
|
||||
// the 0'th item is always a sentinal and not included in this._count.
|
||||
// the 0'th item is always a sentinel and not included in this._count.
|
||||
// The length of the buffer is always this._count + 1
|
||||
//
|
||||
this._count++;
|
||||
@@ -116,7 +116,7 @@ internal class MinHeap<T> : IEnumerable<T> where T : IComparable<T>
|
||||
for (int i = startAt; i < newItemCount; ++i)
|
||||
{
|
||||
//
|
||||
// the 0'th item is always a sentinal and not included in this._count.
|
||||
// the 0'th item is always a sentinel and not included in this._count.
|
||||
// The length of the buffer is always this._count + 1
|
||||
//
|
||||
this._count++;
|
||||
@@ -150,7 +150,7 @@ internal class MinHeap<T> : IEnumerable<T> where T : IComparable<T>
|
||||
{
|
||||
Verify.GreaterThan(capacity, MinCapacity, $"MinHeap capacity must be greater than {MinCapacity}.");
|
||||
|
||||
// 0th item is always a sentinal
|
||||
// 0th item is always a sentinel
|
||||
capacity++;
|
||||
if (capacity > this._items.Length)
|
||||
{
|
||||
@@ -221,7 +221,7 @@ internal class MinHeap<T> : IEnumerable<T> where T : IComparable<T>
|
||||
|
||||
public virtual IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
// The 0'th item in the queue is a sentinal. i is 1 based.
|
||||
// The 0'th item in the queue is a sentinel. i is 1 based.
|
||||
for (int i = 1; i <= this._count; ++i)
|
||||
{
|
||||
yield return this._items[i];
|
||||
@@ -234,19 +234,19 @@ internal class MinHeap<T> : IEnumerable<T> where T : IComparable<T>
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Heap Sort in-place.
|
||||
/// This is destructive. Once you do this, the heap order is lost.
|
||||
/// Heap Sort in-place.
|
||||
/// This is destructive. Once you do this, the heap order is lost.
|
||||
/// The advantage on in-place is that we don't need to do another allocation
|
||||
/// </summary>
|
||||
public void SortDescending()
|
||||
{
|
||||
int count = this._count;
|
||||
int i = count; // remember that the 0'th item in the queue is always a sentinal. So i is 1 based
|
||||
int i = count; // remember that the 0'th item in the queue is always a sentinel. So i is 1 based
|
||||
|
||||
while (this._count > 0)
|
||||
{
|
||||
//
|
||||
// this deques the item with the current LOWEST relevancy
|
||||
// this dequeues the item with the current LOWEST relevancy
|
||||
// We take that and place it at the 'back' of the array - thus inverting it
|
||||
//
|
||||
T item = this.RemoveTop();
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Memory.Collections;
|
||||
/// <summary>
|
||||
/// A collector for Top N matches. Keeps only the best N matches by Score.
|
||||
/// Automatically flushes out any not in the top N.
|
||||
/// By default, items are not sorted by score untily you call <see cref="TopNCollection{T}.SortByScore"/>.
|
||||
/// By default, items are not sorted by score until you call <see cref="TopNCollection{T}.SortByScore"/>.
|
||||
/// </summary>
|
||||
internal class TopNCollection<T> : IEnumerable<ScoredValue<T>>
|
||||
{
|
||||
|
||||
@@ -49,6 +49,28 @@ public interface IReadOnlySkillCollection
|
||||
/// <returns>True if the function exists</returns>
|
||||
bool HasNativeFunction(string functionName);
|
||||
|
||||
/// <summary>
|
||||
/// Return the function delegate stored in the collection, regardless of the function type
|
||||
/// </summary>
|
||||
/// <param name="functionName">Function name</param>
|
||||
/// <returns>Function delegate</returns>
|
||||
ISKFunction GetFunction(string functionName);
|
||||
|
||||
/// <summary>
|
||||
/// Return the function delegate stored in the collection, regardless of the function type
|
||||
/// </summary>
|
||||
/// <param name="functionName">Function name</param>
|
||||
/// <param name="skillName">Skill name</param>
|
||||
/// <returns>Function delegate</returns>
|
||||
ISKFunction GetFunction(string skillName, string functionName);
|
||||
|
||||
/// <summary>
|
||||
/// Return the semantic function delegate stored in the collection
|
||||
/// </summary>
|
||||
/// <param name="functionName">Function name</param>
|
||||
/// <returns>Semantic function delegate</returns>
|
||||
ISKFunction GetSemanticFunction(string functionName);
|
||||
|
||||
/// <summary>
|
||||
/// Return the semantic function delegate stored in the collection
|
||||
/// </summary>
|
||||
|
||||
@@ -46,6 +46,24 @@ internal class ReadOnlySkillCollection : IReadOnlySkillCollection
|
||||
return this._skillCollection.HasNativeFunction(functionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISKFunction GetFunction(string functionName)
|
||||
{
|
||||
return this._skillCollection.GetFunction(functionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISKFunction GetFunction(string skillName, string functionName)
|
||||
{
|
||||
return this._skillCollection.GetFunction(skillName, functionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISKFunction GetSemanticFunction(string functionName)
|
||||
{
|
||||
return this._skillCollection.GetSemanticFunction(functionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISKFunction GetSemanticFunction(string skillName, string functionName)
|
||||
{
|
||||
|
||||
@@ -96,6 +96,32 @@ public class SkillCollection : ISkillCollection
|
||||
return this.HasNativeFunction(GlobalSkill, functionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISKFunction GetFunction(string functionName)
|
||||
{
|
||||
return this.GetFunction(GlobalSkill, functionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISKFunction GetFunction(string skillName, string functionName)
|
||||
{
|
||||
if (this.HasFunction(skillName, functionName))
|
||||
{
|
||||
return this._skillCollection[skillName][functionName];
|
||||
}
|
||||
|
||||
this._log.LogError("Function not available: skill:{0} function:{1}", skillName, functionName);
|
||||
throw new KernelException(
|
||||
KernelException.ErrorCodes.FunctionNotAvailable,
|
||||
$"Function not available {skillName}.{functionName}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISKFunction GetSemanticFunction(string functionName)
|
||||
{
|
||||
return this.GetSemanticFunction(GlobalSkill, functionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISKFunction GetSemanticFunction(string skillName, string functionName)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
@@ -15,7 +12,12 @@ public abstract class Block
|
||||
{
|
||||
internal virtual BlockTypes Type => BlockTypes.Undefined;
|
||||
|
||||
internal string Content { get; set; } = string.Empty;
|
||||
// internal virtual bool? SynchronousRendering => null;
|
||||
|
||||
/// <summary>
|
||||
/// The block content
|
||||
/// </summary>
|
||||
internal string Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// App logger
|
||||
@@ -25,18 +27,19 @@ public abstract class Block
|
||||
/// <summary>
|
||||
/// Base constructor
|
||||
/// </summary>
|
||||
/// <param name="content">Block content</param>
|
||||
/// <param name="log">App logger</param>
|
||||
protected Block(ILogger? log = null)
|
||||
protected Block(string? content, ILogger? log = null)
|
||||
{
|
||||
if (log != null) { this.Log = log; }
|
||||
|
||||
this.Content = content ?? string.Empty;
|
||||
}
|
||||
|
||||
internal virtual Task<string> RenderCodeAsync(SKContext executionContext)
|
||||
{
|
||||
throw new NotImplementedException("This block doesn't support code execution");
|
||||
}
|
||||
|
||||
internal abstract bool IsValid(out string error);
|
||||
|
||||
internal abstract string Render(ContextVariables? variables);
|
||||
/// <summary>
|
||||
/// Check if the block content is valid.
|
||||
/// </summary>
|
||||
/// <param name="errorMsg">Error message in case the content is not valid</param>
|
||||
/// <returns>True if the block content is valid</returns>
|
||||
public abstract bool IsValid(out string errorMsg);
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ internal enum BlockTypes
|
||||
Text = 1,
|
||||
Code = 2,
|
||||
Variable = 3,
|
||||
Value = 4,
|
||||
FunctionId = 5,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
@@ -13,87 +10,66 @@ using Microsoft.SemanticKernel.SkillDefinition;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
internal class CodeBlock : Block
|
||||
#pragma warning disable CA2254 // error strings are used also internally, not just for logging
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
internal class CodeBlock : Block, ICodeRendering
|
||||
{
|
||||
internal override BlockTypes Type => BlockTypes.Code;
|
||||
|
||||
internal CodeBlock(string content, ILogger log) : base(log)
|
||||
public CodeBlock(string? content, ILogger log)
|
||||
: this(new CodeTokenizer(log).Tokenize(content), content?.Trim(), log)
|
||||
{
|
||||
this.Content = content;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2254 // error strings are used also internally, not just for logging
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
internal override bool IsValid(out string error)
|
||||
public CodeBlock(List<Block> tokens, string? content, ILogger log)
|
||||
: base(content?.Trim(), log)
|
||||
{
|
||||
error = "";
|
||||
this._tokens = tokens;
|
||||
}
|
||||
|
||||
List<string> partsToValidate = this.Content.Split(' ', '\t', '\r', '\n')
|
||||
.Where(x => !string.IsNullOrEmpty(x.Trim()))
|
||||
.ToList();
|
||||
public override bool IsValid(out string errorMsg)
|
||||
{
|
||||
errorMsg = "";
|
||||
|
||||
for (var index = 0; index < partsToValidate.Count; index++)
|
||||
foreach (Block token in this._tokens)
|
||||
{
|
||||
var part = partsToValidate[index];
|
||||
|
||||
if (index == 0) // There is only a function name
|
||||
if (!token.IsValid(out errorMsg))
|
||||
{
|
||||
if (VarBlock.HasVarPrefix(part))
|
||||
{
|
||||
error = $"Variables cannot be used as function names [`{part}`]";
|
||||
|
||||
this.Log.LogError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Regex.IsMatch(part, "^[a-zA-Z0-9_.]*$"))
|
||||
{
|
||||
error = $"The function name `{part}` contains invalid characters";
|
||||
this.Log.LogError(error);
|
||||
return false;
|
||||
}
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
else // The function has parameters
|
||||
}
|
||||
|
||||
if (this._tokens.Count > 1)
|
||||
{
|
||||
if (this._tokens[0].Type != BlockTypes.FunctionId)
|
||||
{
|
||||
if (!VarBlock.HasVarPrefix(part))
|
||||
{
|
||||
error = $"`{part}` is not a valid function parameter: parameters must be variables.";
|
||||
this.Log.LogError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (part.Length < 2)
|
||||
{
|
||||
error = $"`{part}` is not a valid variable.";
|
||||
this.Log.LogError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VarBlock.IsValidVarName(part.Substring(1)))
|
||||
{
|
||||
error = $"`{part}` variable name is not valid.";
|
||||
this.Log.LogError(error);
|
||||
return false;
|
||||
}
|
||||
errorMsg = $"Unexpected second token found: {this._tokens[1].Content}";
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._tokens[1].Type != BlockTypes.Value && this._tokens[1].Type != BlockTypes.Variable)
|
||||
{
|
||||
errorMsg = "Functions support only one parameter";
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._tokens.Count > 2)
|
||||
{
|
||||
errorMsg = $"Unexpected second token found: {this._tokens[1].Content}";
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
this._validated = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
// ReSharper restore TemplateIsNotCompileTimeConstantProblem
|
||||
#pragma warning restore CA2254
|
||||
|
||||
internal override string Render(ContextVariables? variables)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Code blocks rendering requires IReadOnlySkillCollection. Incorrect method call.");
|
||||
}
|
||||
|
||||
#pragma warning disable CA2254 // error strings are used also internally, not just for logging
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
internal override async Task<string> RenderCodeAsync(SKContext context)
|
||||
public async Task<string> RenderCodeAsync(SKContext context)
|
||||
{
|
||||
if (!this._validated && !this.IsValid(out var error))
|
||||
{
|
||||
@@ -102,33 +78,48 @@ internal class CodeBlock : Block
|
||||
|
||||
this.Log.LogTrace("Rendering code: `{0}`", this.Content);
|
||||
|
||||
List<string> parts = this.Content.Split(' ', '\t', '\r', '\n')
|
||||
.Where(x => !string.IsNullOrEmpty(x.Trim()))
|
||||
.ToList();
|
||||
|
||||
var functionName = parts[0];
|
||||
context.ThrowIfSkillCollectionNotSet();
|
||||
if (!this.GetFunctionFromSkillCollection(context.Skills!, functionName, out ISKFunction? function))
|
||||
switch (this._tokens[0].Type)
|
||||
{
|
||||
var errorMsg = $"Function not found `{functionName}`";
|
||||
case BlockTypes.Value:
|
||||
case BlockTypes.Variable:
|
||||
return ((ITextRendering)this._tokens[0]).Render(context.Variables);
|
||||
|
||||
case BlockTypes.FunctionId:
|
||||
return await this.RenderFunctionCallAsync((FunctionIdBlock)this._tokens[0], context);
|
||||
}
|
||||
|
||||
throw new TemplateException(TemplateException.ErrorCodes.UnexpectedBlockType,
|
||||
$"Unexpected first token type: {this._tokens[0].Type:G}");
|
||||
}
|
||||
|
||||
#region private ================================================================================
|
||||
|
||||
private bool _validated;
|
||||
private readonly List<Block> _tokens;
|
||||
|
||||
private async Task<string> RenderFunctionCallAsync(FunctionIdBlock fBlock, SKContext context)
|
||||
{
|
||||
context.ThrowIfSkillCollectionNotSet();
|
||||
if (!this.GetFunctionFromSkillCollection(context.Skills!, fBlock, out ISKFunction? function))
|
||||
{
|
||||
var errorMsg = $"Function `{fBlock.Content}` not found";
|
||||
this.Log.LogError(errorMsg);
|
||||
throw new TemplateException(TemplateException.ErrorCodes.FunctionNotFound, errorMsg);
|
||||
}
|
||||
|
||||
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
|
||||
// Using $input by default, e.g. when the syntax is {{functionName}}
|
||||
|
||||
// TODO: unit test, verify that all context variables are passed to Render()
|
||||
ContextVariables variablesClone = context.Variables.Clone();
|
||||
if (parts.Count > 1)
|
||||
|
||||
// If the code syntax is {{functionName $varName}} use $varName instead of $input
|
||||
// If the code syntax is {{functionName 'value'}} use "value" instead of $input
|
||||
if (this._tokens.Count > 1)
|
||||
{
|
||||
this.Log.LogTrace("Passing required variable: `{0}`", parts[1]);
|
||||
// If the code syntax is {{functionName $varName}} use $varName instead of $input
|
||||
string value = new VarBlock(parts[1], this.Log).Render(variablesClone);
|
||||
variablesClone.Update(value);
|
||||
// TODO: PII
|
||||
this.Log.LogTrace("Passing variable/value: `{0}`", this._tokens[1].Content);
|
||||
string input = ((ITextRendering)this._tokens[1]).Render(variablesClone);
|
||||
variablesClone.Update(input);
|
||||
}
|
||||
|
||||
var result = await function.InvokeWithCustomInputAsync(
|
||||
SKContext result = await function.InvokeWithCustomInputAsync(
|
||||
variablesClone,
|
||||
context.Memory,
|
||||
context.Skills,
|
||||
@@ -137,56 +128,31 @@ internal class CodeBlock : Block
|
||||
|
||||
if (result.ErrorOccurred)
|
||||
{
|
||||
var errorMsg = $"Function `{functionName}` execution failed. {result.LastException?.GetType().FullName}: {result.LastErrorDescription}";
|
||||
var errorMsg = $"Function `{fBlock.Content}` execution failed. {result.LastException?.GetType().FullName}: {result.LastErrorDescription}";
|
||||
this.Log.LogError(errorMsg);
|
||||
throw new TemplateException(TemplateException.ErrorCodes.RuntimeError, errorMsg, result.LastException);
|
||||
}
|
||||
|
||||
return result.Result;
|
||||
}
|
||||
// ReSharper restore TemplateIsNotCompileTimeConstantProblem
|
||||
#pragma warning restore CA2254
|
||||
|
||||
#region private ================================================================================
|
||||
|
||||
private bool _validated;
|
||||
|
||||
private bool GetFunctionFromSkillCollection(IReadOnlySkillCollection skills, string functionName,
|
||||
private bool GetFunctionFromSkillCollection(
|
||||
IReadOnlySkillCollection skills,
|
||||
FunctionIdBlock fBlock,
|
||||
[NotNullWhen(true)] out ISKFunction? function)
|
||||
{
|
||||
// Search in the global space (only native functions there)
|
||||
if (skills.HasNativeFunction(functionName))
|
||||
// Function in the global skill
|
||||
if (string.IsNullOrEmpty(fBlock.SkillName) && skills.HasFunction(fBlock.FunctionName))
|
||||
{
|
||||
function = skills.GetNativeFunction(functionName);
|
||||
function = skills.GetFunction(fBlock.FunctionName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the function contains a skill name...
|
||||
if (functionName.Contains('.', StringComparison.InvariantCulture))
|
||||
// Function within a specific skill
|
||||
if (!string.IsNullOrEmpty(fBlock.SkillName) && skills.HasFunction(fBlock.SkillName, fBlock.FunctionName))
|
||||
{
|
||||
var functionNameParts = functionName.Split('.');
|
||||
if (functionNameParts.Length > 2)
|
||||
{
|
||||
this.Log.LogError("Invalid function name `{0}`", functionName);
|
||||
throw new ArgumentOutOfRangeException(
|
||||
$"Invalid function name `{functionName}`. " +
|
||||
"A Function name can contain only one `.` to separate skill name from function name.");
|
||||
}
|
||||
|
||||
var skillName = functionNameParts[0];
|
||||
functionName = functionNameParts[1];
|
||||
|
||||
if (skills.HasNativeFunction(skillName, functionName))
|
||||
{
|
||||
function = skills.GetNativeFunction(skillName, functionName);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (skills.HasSemanticFunction(skillName, functionName))
|
||||
{
|
||||
function = skills.GetSemanticFunction(skillName, functionName);
|
||||
return true;
|
||||
}
|
||||
function = skills.GetFunction(fBlock.SkillName, fBlock.FunctionName);
|
||||
return true;
|
||||
}
|
||||
|
||||
function = null;
|
||||
@@ -195,3 +161,5 @@ internal class CodeBlock : Block
|
||||
|
||||
#endregion
|
||||
}
|
||||
// ReSharper restore TemplateIsNotCompileTimeConstantProblem
|
||||
#pragma warning restore CA2254
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
internal class FunctionIdBlock : Block, ITextRendering
|
||||
{
|
||||
internal override BlockTypes Type => BlockTypes.FunctionId;
|
||||
|
||||
internal string SkillName { get; } = string.Empty;
|
||||
|
||||
internal string FunctionName { get; } = string.Empty;
|
||||
|
||||
public FunctionIdBlock(string? text, ILogger? log = null)
|
||||
: base(text?.Trim(), log)
|
||||
{
|
||||
var functionNameParts = this.Content.Split('.');
|
||||
if (functionNameParts.Length > 2)
|
||||
{
|
||||
this.Log.LogError("Invalid function name `{0}`", this.Content);
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError,
|
||||
"A function name can contain at most one dot separating the skill name from the function name");
|
||||
}
|
||||
|
||||
if (functionNameParts.Length == 2)
|
||||
{
|
||||
this.SkillName = functionNameParts[0];
|
||||
this.FunctionName = functionNameParts[1];
|
||||
return;
|
||||
}
|
||||
|
||||
this.FunctionName = this.Content;
|
||||
}
|
||||
|
||||
public override bool IsValid(out string errorMsg)
|
||||
{
|
||||
if (!Regex.IsMatch(this.Content, "^[a-zA-Z0-9_.]*$"))
|
||||
{
|
||||
errorMsg = "The function identifier is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HasMoreThanOneDot(this.Content))
|
||||
{
|
||||
errorMsg = "The function identifier can contain max one '.' char separating skill name from function name";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMsg = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
public string Render(ContextVariables? variables)
|
||||
{
|
||||
return this.Content;
|
||||
}
|
||||
|
||||
private static bool HasMoreThanOneDot(string? value)
|
||||
{
|
||||
if (value == null || value.Length < 2) { return false; }
|
||||
|
||||
int count = 0;
|
||||
return value.Any(t => t == '.' && ++count > 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of dynamic blocks that need async IO to be rendered.
|
||||
/// </summary>
|
||||
public interface ICodeRendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Render the block using the given context, potentially using external I/O.
|
||||
/// </summary>
|
||||
/// <param name="context">SK execution context</param>
|
||||
/// <returns>Rendered content</returns>
|
||||
internal Task<string> RenderCodeAsync(SKContext context);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of static blocks that don't need async IO to be rendered.
|
||||
/// </summary>
|
||||
public interface ITextRendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Render the block using only the given variables.
|
||||
/// </summary>
|
||||
/// <param name="variables">Optional variables used to render the block</param>
|
||||
/// <returns>Rendered content</returns>
|
||||
internal string Render(ContextVariables? variables);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
internal static class Symbols
|
||||
{
|
||||
internal const char BlockStarter = '{';
|
||||
internal const char BlockEnder = '}';
|
||||
|
||||
internal const char VarPrefix = '$';
|
||||
|
||||
internal const char DblQuote = '"';
|
||||
internal const char SglQuote = '\'';
|
||||
internal const char EscapeChar = '\\';
|
||||
|
||||
internal const char Space = ' ';
|
||||
internal const char Tab = '\t';
|
||||
internal const char NewLine = '\n';
|
||||
internal const char CarriageReturn = '\r';
|
||||
}
|
||||
@@ -5,29 +5,27 @@ using Microsoft.SemanticKernel.Orchestration;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
internal class TextBlock : Block
|
||||
internal class TextBlock : Block, ITextRendering
|
||||
{
|
||||
internal override BlockTypes Type => BlockTypes.Text;
|
||||
|
||||
internal TextBlock(string content, ILogger? log = null)
|
||||
: base(log)
|
||||
public TextBlock(string? text, ILogger? log = null)
|
||||
: base(text, log)
|
||||
{
|
||||
this.Content = content;
|
||||
}
|
||||
|
||||
internal TextBlock(string text, int startIndex, int stopIndex, ILogger log)
|
||||
: base(log)
|
||||
public TextBlock(string text, int startIndex, int stopIndex, ILogger log)
|
||||
: base(text.Substring(startIndex, stopIndex - startIndex), log)
|
||||
{
|
||||
this.Content = text.Substring(startIndex, stopIndex - startIndex);
|
||||
}
|
||||
|
||||
internal override bool IsValid(out string error)
|
||||
public override bool IsValid(out string errorMsg)
|
||||
{
|
||||
error = "";
|
||||
errorMsg = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
internal override string Render(ContextVariables? variables)
|
||||
public string Render(ContextVariables? variables)
|
||||
{
|
||||
return this.Content;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
internal class ValBlock : Block, ITextRendering
|
||||
{
|
||||
internal override BlockTypes Type => BlockTypes.Value;
|
||||
|
||||
// Cache the first and last char
|
||||
private readonly char _first = '\0';
|
||||
private readonly char _last = '\0';
|
||||
|
||||
// Content, excluding start/end quote chars
|
||||
private readonly string _value = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance
|
||||
/// </summary>
|
||||
/// <param name="quotedValue">Block content, including the delimiting chars</param>
|
||||
/// <param name="log">Optional logger</param>
|
||||
public ValBlock(string? quotedValue, ILogger? log = null)
|
||||
: base(quotedValue?.Trim(), log)
|
||||
{
|
||||
if (this.Content.Length < 2)
|
||||
{
|
||||
this.Log.LogError("A value must have single quotes or double quotes on both sides");
|
||||
return;
|
||||
}
|
||||
|
||||
this._first = this.Content[0];
|
||||
this._last = this.Content[^1];
|
||||
this._value = this.Content[1..^1];
|
||||
}
|
||||
|
||||
#pragma warning disable CA2254 // error strings are used also internally, not just for logging
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
public override bool IsValid(out string errorMsg)
|
||||
{
|
||||
errorMsg = string.Empty;
|
||||
|
||||
// Content includes the quotes, so it must be at least 2 chars long
|
||||
if (this.Content.Length < 2)
|
||||
{
|
||||
errorMsg = "A value must have single quotes or double quotes on both sides";
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if delimiting chars are consistent
|
||||
if (this._first != this._last)
|
||||
{
|
||||
errorMsg = "A value must be defined using either single quotes or double quotes, not both";
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#pragma warning restore CA2254
|
||||
|
||||
public string Render(ContextVariables? variables)
|
||||
{
|
||||
return this._value;
|
||||
}
|
||||
|
||||
public static bool HasValPrefix(string? text)
|
||||
{
|
||||
return !string.IsNullOrEmpty(text)
|
||||
&& text.Length > 0
|
||||
&& (text[0] is Symbols.DblQuote or Symbols.SglQuote);
|
||||
}
|
||||
}
|
||||
@@ -4,48 +4,57 @@ using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
internal class VarBlock : Block
|
||||
internal class VarBlock : Block, ITextRendering
|
||||
{
|
||||
private const char Prefix = '$';
|
||||
|
||||
internal override BlockTypes Type => BlockTypes.Variable;
|
||||
|
||||
internal string Name => this.VarName();
|
||||
internal string Name { get; } = string.Empty;
|
||||
|
||||
internal VarBlock(string content, ILogger? log = null) : base(log)
|
||||
public VarBlock(string? content, ILogger? log = null) : base(content?.Trim(), log)
|
||||
{
|
||||
this.Content = content;
|
||||
if (this.Content.Length < 2)
|
||||
{
|
||||
this.Log.LogError("The variable name is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
this.Name = this.Content[1..];
|
||||
}
|
||||
|
||||
#pragma warning disable CA2254 // error strings are used also internally, not just for logging
|
||||
internal override bool IsValid(out string error)
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
public override bool IsValid(out string errorMsg)
|
||||
{
|
||||
error = string.Empty;
|
||||
errorMsg = string.Empty;
|
||||
|
||||
if (this.Content[0] != Prefix)
|
||||
if (string.IsNullOrEmpty(this.Content))
|
||||
{
|
||||
error = $"A variable must start with the symbol {Prefix}";
|
||||
this.Log.LogError(error);
|
||||
errorMsg = $"A variable must start with the symbol {Symbols.VarPrefix} and have a name";
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.Content[0] != Symbols.VarPrefix)
|
||||
{
|
||||
errorMsg = $"A variable must start with the symbol {Symbols.VarPrefix}";
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.Content.Length < 2)
|
||||
{
|
||||
error = "The variable name is empty";
|
||||
this.Log.LogError(error);
|
||||
errorMsg = "The variable name is empty";
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
var varName = this.VarName();
|
||||
if (!Regex.IsMatch(varName, "^[a-zA-Z0-9_]*$"))
|
||||
if (!Regex.IsMatch(this.Name, "^[a-zA-Z0-9_]*$"))
|
||||
{
|
||||
error = $"The variable name '{varName}' contains invalid characters. " +
|
||||
errorMsg = $"The variable name '{this.Name}' contains invalid characters. " +
|
||||
"Only alphanumeric chars and underscore are allowed.";
|
||||
this.Log.LogError(error);
|
||||
this.Log.LogError(errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -53,36 +62,20 @@ internal class VarBlock : Block
|
||||
}
|
||||
#pragma warning restore CA2254
|
||||
|
||||
internal override string Render(ContextVariables? variables)
|
||||
public string Render(ContextVariables? variables)
|
||||
{
|
||||
if (variables == null) { return string.Empty; }
|
||||
|
||||
var name = this.VarName();
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
if (string.IsNullOrEmpty(this.Name))
|
||||
{
|
||||
var exists = variables.Get(name, out string value);
|
||||
if (!exists) { this.Log.LogWarning("Variable `{0}{1}` not found", Prefix, name); }
|
||||
|
||||
return exists ? value : "";
|
||||
const string errMsg = "Variable rendering failed, the variable name is empty";
|
||||
this.Log.LogError(errMsg);
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError, errMsg);
|
||||
}
|
||||
|
||||
this.Log.LogError("Variable rendering failed, the variable name is empty");
|
||||
throw new TemplateException(
|
||||
TemplateException.ErrorCodes.SyntaxError, "Variable rendering failed, the variable name is empty.");
|
||||
}
|
||||
var exists = variables.Get(this.Name, out string value);
|
||||
if (!exists) { this.Log.LogWarning("Variable `{0}{1}` not found", Symbols.VarPrefix, this.Name); }
|
||||
|
||||
internal static bool HasVarPrefix(string text)
|
||||
{
|
||||
return !string.IsNullOrEmpty(text) && text.Length > 0 && text[0] == Prefix;
|
||||
}
|
||||
|
||||
internal static bool IsValidVarName(string text)
|
||||
{
|
||||
return Regex.IsMatch(text, "^[a-zA-Z0-9_]*$");
|
||||
}
|
||||
|
||||
private string VarName()
|
||||
{
|
||||
return this.Content.Length < 2 ? "" : this.Content[1..];
|
||||
return exists ? value : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Simple tokenizer used for default SK template code language.
|
||||
///
|
||||
/// BNF parsed by TemplateTokenizer:
|
||||
/// [template] ::= "" | [block] | [block] [template]
|
||||
/// [block] ::= [sk-block] | [text-block]
|
||||
/// [sk-block] ::= "{{" [variable] "}}" | "{{" [value] "}}" | "{{" [function-call] "}}"
|
||||
/// [text-block] ::= [any-char] | [any-char] [text-block]
|
||||
/// [any-char] ::= any char
|
||||
///
|
||||
/// BNF parsed by CodeTokenizer:
|
||||
/// [template] ::= "" | [variable] " " [template] | [value] " " [template] | [function-call] " " [template]
|
||||
/// [variable] ::= "$" [valid-name]
|
||||
/// [value] ::= "'" [text] "'" | '"' [text] '"'
|
||||
/// [function-call] ::= [function-id] | [function-id] [parameter]
|
||||
/// [parameter] ::= [variable] | [value]
|
||||
///
|
||||
/// BNF parsed by dedicated blocks
|
||||
/// [function-id] ::= [valid-name] | [valid-name] "." [valid-name]
|
||||
/// [valid-name] ::= [valid-symbol] | [valid-symbol] [valid-name]
|
||||
/// [valid-symbol] ::= [letter] | [digit] | "_"
|
||||
/// [letter] ::= "a" | "b" ... | "z" | "A" | "B" ... | "Z"
|
||||
/// [digit] ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
|
||||
/// </summary>
|
||||
internal class CodeTokenizer
|
||||
{
|
||||
private enum TokenTypes
|
||||
{
|
||||
None = 0,
|
||||
Value = 1,
|
||||
Variable = 2,
|
||||
FunctionId = 3,
|
||||
}
|
||||
|
||||
private readonly ILogger _log;
|
||||
|
||||
public CodeTokenizer(ILogger? log = null)
|
||||
{
|
||||
this._log = log ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tokenize a code block, without checking for syntax errors
|
||||
/// </summary>
|
||||
/// <param name="text">Text to parse</param>
|
||||
/// <returns>A list of blocks</returns>
|
||||
public List<Block> Tokenize(string? text)
|
||||
{
|
||||
// Remove spaces, which are ignored anyway
|
||||
text = text?.Trim();
|
||||
|
||||
// Render NULL to ""
|
||||
if (string.IsNullOrEmpty(text)) { return new List<Block>(); }
|
||||
|
||||
// Track what type of token we're reading
|
||||
TokenTypes currentTokenType = TokenTypes.None;
|
||||
|
||||
// Track the content of the current token
|
||||
var currentTokenContent = new StringBuilder();
|
||||
|
||||
char textValueDelimiter = '\0';
|
||||
|
||||
var blocks = new List<Block>();
|
||||
char nextChar = text[0];
|
||||
|
||||
// Tokens must be separated by spaces, track their presence
|
||||
bool spaceSeparatorFound = false;
|
||||
|
||||
// 1 char only edge case
|
||||
if (text.Length == 1)
|
||||
{
|
||||
switch (nextChar)
|
||||
{
|
||||
case Symbols.VarPrefix:
|
||||
blocks.Add(new VarBlock(text, this._log));
|
||||
break;
|
||||
|
||||
case Symbols.DblQuote:
|
||||
case Symbols.SglQuote:
|
||||
blocks.Add(new ValBlock(text, this._log));
|
||||
break;
|
||||
|
||||
default:
|
||||
blocks.Add(new FunctionIdBlock(text, this._log));
|
||||
break;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
bool skipNextChar = false;
|
||||
for (int nextCharCursor = 1; nextCharCursor < text.Length; nextCharCursor++)
|
||||
{
|
||||
char currentChar = nextChar;
|
||||
nextChar = text[nextCharCursor];
|
||||
|
||||
if (skipNextChar)
|
||||
{
|
||||
skipNextChar = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// First char is easy
|
||||
if (nextCharCursor == 1)
|
||||
{
|
||||
if (IsVarPrefix(currentChar))
|
||||
{
|
||||
currentTokenType = TokenTypes.Variable;
|
||||
}
|
||||
else if (IsQuote(currentChar))
|
||||
{
|
||||
currentTokenType = TokenTypes.Value;
|
||||
textValueDelimiter = currentChar;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentTokenType = TokenTypes.FunctionId;
|
||||
}
|
||||
|
||||
currentTokenContent.Append(currentChar);
|
||||
continue;
|
||||
}
|
||||
|
||||
// While reading a values between quotes
|
||||
if (currentTokenType == TokenTypes.Value)
|
||||
{
|
||||
// If the current char is escaping the next special char:
|
||||
// - skip the current char (escape char)
|
||||
// - add the next (special char)
|
||||
// - jump to the one after (to handle "\\" properly)
|
||||
if (currentChar == Symbols.EscapeChar && CanBeEscaped(nextChar))
|
||||
{
|
||||
currentTokenContent.Append(nextChar);
|
||||
skipNextChar = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
currentTokenContent.Append(currentChar);
|
||||
|
||||
// When we reach the end of the value
|
||||
if (currentChar == textValueDelimiter)
|
||||
{
|
||||
blocks.Add(new ValBlock(currentTokenContent.ToString(), this._log));
|
||||
currentTokenContent.Clear();
|
||||
currentTokenType = TokenTypes.None;
|
||||
spaceSeparatorFound = false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're not between quotes, a space signals the end of the current token
|
||||
// Note: there might be multiple consecutive spaces
|
||||
if (IsBlankSpace(currentChar))
|
||||
{
|
||||
if (currentTokenType == TokenTypes.Variable)
|
||||
{
|
||||
blocks.Add(new VarBlock(currentTokenContent.ToString(), this._log));
|
||||
currentTokenContent.Clear();
|
||||
}
|
||||
else if (currentTokenType == TokenTypes.FunctionId)
|
||||
{
|
||||
blocks.Add(new FunctionIdBlock(currentTokenContent.ToString(), this._log));
|
||||
currentTokenContent.Clear();
|
||||
}
|
||||
|
||||
spaceSeparatorFound = true;
|
||||
currentTokenType = TokenTypes.None;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're not inside a quoted value and we're not processing a space
|
||||
currentTokenContent.Append(currentChar);
|
||||
|
||||
if (currentTokenType == TokenTypes.None)
|
||||
{
|
||||
if (!spaceSeparatorFound)
|
||||
{
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError,
|
||||
"Tokens must be separated by one space least");
|
||||
}
|
||||
|
||||
if (IsQuote(currentChar))
|
||||
{
|
||||
// A quoted value starts here
|
||||
currentTokenType = TokenTypes.Value;
|
||||
textValueDelimiter = currentChar;
|
||||
}
|
||||
else if (IsVarPrefix(currentChar))
|
||||
{
|
||||
// A variable starts here
|
||||
currentTokenType = TokenTypes.Variable;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A function Id starts here
|
||||
currentTokenType = TokenTypes.FunctionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture last token
|
||||
currentTokenContent.Append(nextChar);
|
||||
switch (currentTokenType)
|
||||
{
|
||||
case TokenTypes.Value:
|
||||
blocks.Add(new ValBlock(currentTokenContent.ToString(), this._log));
|
||||
break;
|
||||
|
||||
case TokenTypes.Variable:
|
||||
blocks.Add(new VarBlock(currentTokenContent.ToString(), this._log));
|
||||
break;
|
||||
|
||||
case TokenTypes.FunctionId:
|
||||
blocks.Add(new FunctionIdBlock(currentTokenContent.ToString(), this._log));
|
||||
break;
|
||||
|
||||
case TokenTypes.None:
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError,
|
||||
"Tokens must be separated by one space least");
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static bool IsVarPrefix(char c)
|
||||
{
|
||||
return (c == Symbols.VarPrefix);
|
||||
}
|
||||
|
||||
private static bool IsBlankSpace(char c)
|
||||
{
|
||||
return c is Symbols.Space or Symbols.NewLine or Symbols.CarriageReturn or Symbols.Tab;
|
||||
}
|
||||
|
||||
private static bool IsQuote(char c)
|
||||
{
|
||||
return c is Symbols.DblQuote or Symbols.SglQuote;
|
||||
}
|
||||
|
||||
private static bool CanBeEscaped(char c)
|
||||
{
|
||||
return c is Symbols.DblQuote or Symbols.SglQuote or Symbols.EscapeChar;
|
||||
}
|
||||
}
|
||||
@@ -27,21 +27,21 @@ public interface IPromptTemplateEngine
|
||||
/// reference with the function result.
|
||||
/// </summary>
|
||||
/// <param name="templateText">Prompt template (see skprompt.txt files)</param>
|
||||
/// <param name="executionContext">Access into the current kernel execution context</param>
|
||||
/// <param name="context">Access into the current kernel execution context</param>
|
||||
/// <returns>The prompt template ready to be used for an AI request</returns>
|
||||
Task<string> RenderAsync(
|
||||
string templateText,
|
||||
SKContext executionContext);
|
||||
SKContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Given a a list of blocks render each block and compose the final result
|
||||
/// Given a list of blocks render each block and compose the final result
|
||||
/// </summary>
|
||||
/// <param name="blocks">Template blocks generated by ExtractBlocks</param>
|
||||
/// <param name="executionContext">Access into the current kernel execution context</param>
|
||||
/// <param name="context">Access into the current kernel execution context</param>
|
||||
/// <returns>The prompt template ready to be used for an AI request</returns>
|
||||
Task<string> RenderAsync(
|
||||
IList<Block> blocks,
|
||||
SKContext executionContext);
|
||||
SKContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of blocks, render the Variable Blocks, replacing placeholders with the actual value in memory
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@@ -24,100 +23,83 @@ namespace Microsoft.SemanticKernel.TemplateEngine;
|
||||
/// </summary>
|
||||
public class PromptTemplateEngine : IPromptTemplateEngine
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
|
||||
private readonly TemplateTokenizer _tokenizer;
|
||||
|
||||
public PromptTemplateEngine(ILogger? log = null)
|
||||
{
|
||||
this._log = log ?? NullLogger.Instance;
|
||||
this._tokenizer = new TemplateTokenizer(this._log);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a prompt template string, extract all the blocks (text, variables, function calls)
|
||||
/// </summary>
|
||||
/// <param name="templateText">Prompt template (see skprompt.txt files)</param>
|
||||
/// <param name="validate">Whether to validate the blocks syntax, or just return the blocks found, which could contain invalid code</param>
|
||||
/// <returns>A list of all the blocks, ie the template tokenized in text, variables and function calls</returns>
|
||||
/// <inheritdoc/>
|
||||
public IList<Block> ExtractBlocks(string? templateText, bool validate = true)
|
||||
{
|
||||
this._log.LogTrace("Extracting blocks from template: {0}", templateText);
|
||||
var blocks = this.TokenizeInternal(templateText);
|
||||
if (validate) { ValidateBlocksSyntax(blocks); }
|
||||
var blocks = this._tokenizer.Tokenize(templateText);
|
||||
|
||||
if (validate)
|
||||
{
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
if (!block.IsValid(out var error))
|
||||
{
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a prompt template, replace the variables with their values and execute the functions replacing their
|
||||
/// reference with the function result.
|
||||
/// </summary>
|
||||
/// <param name="templateText">Prompt template (see skprompt.txt files)</param>
|
||||
/// <param name="executionContext">Access into the current kernel execution context</param>
|
||||
/// <returns>The prompt template ready to be used for an AI request</returns>
|
||||
public async Task<string> RenderAsync(
|
||||
string templateText,
|
||||
SKContext executionContext)
|
||||
/// <inheritdoc/>
|
||||
public async Task<string> RenderAsync(string templateText, SKContext context)
|
||||
{
|
||||
this._log.LogTrace("Rendering string template: {0}", templateText);
|
||||
var blocks = this.ExtractBlocks(templateText);
|
||||
return await this.RenderAsync(blocks, executionContext);
|
||||
return await this.RenderAsync(blocks, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a a list of blocks render each block and compose the final result
|
||||
/// </summary>
|
||||
/// <param name="blocks">Template blocks generated by ExtractBlocks</param>
|
||||
/// <param name="executionContext">Access into the current kernel execution context</param>
|
||||
/// <returns>The prompt template ready to be used for an AI request</returns>
|
||||
public async Task<string> RenderAsync(
|
||||
IList<Block> blocks,
|
||||
SKContext executionContext)
|
||||
/// <inheritdoc/>
|
||||
public async Task<string> RenderAsync(IList<Block> blocks, SKContext context)
|
||||
{
|
||||
this._log.LogTrace("Rendering list of {0} blocks", blocks.Count);
|
||||
var result = new StringBuilder();
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
switch (block.Type)
|
||||
switch (block)
|
||||
{
|
||||
case BlockTypes.Text:
|
||||
result.Append(block.Content);
|
||||
case ITextRendering staticBlock:
|
||||
result.Append(staticBlock.Render(context.Variables));
|
||||
break;
|
||||
|
||||
case BlockTypes.Variable:
|
||||
result.Append(block.Render(executionContext.Variables));
|
||||
case ICodeRendering dynamicBlock:
|
||||
result.Append(await dynamicBlock.RenderCodeAsync(context));
|
||||
break;
|
||||
|
||||
case BlockTypes.Code:
|
||||
result.Append(await block.RenderCodeAsync(executionContext));
|
||||
break;
|
||||
|
||||
case BlockTypes.Undefined:
|
||||
default:
|
||||
throw new InvalidEnumArgumentException(nameof(blocks), (int)block.Type, typeof(BlockTypes));
|
||||
const string error = "Unexpected block type, the block doesn't have a rendering method";
|
||||
this._log.LogError(error);
|
||||
throw new TemplateException(TemplateException.ErrorCodes.UnexpectedBlockType, error);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove PII, allow tracing prompts differently
|
||||
this._log.LogDebug("Rendered prompt: {0}", result);
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of blocks, render the Variable Blocks, replacing placeholders with the actual value in memory
|
||||
/// </summary>
|
||||
/// <param name="blocks">List of blocks, typically all the blocks found in a template</param>
|
||||
/// <param name="variables">Container of all the temporary variables known to the kernel</param>
|
||||
/// <returns>An updated list of blocks where Variable Blocks have rendered to Text Blocks</returns>
|
||||
/// <inheritdoc/>
|
||||
public IList<Block> RenderVariables(IList<Block> blocks, ContextVariables? variables)
|
||||
{
|
||||
this._log.LogTrace("Rendering variables");
|
||||
return blocks.Select(block => block.Type != BlockTypes.Variable
|
||||
? block
|
||||
: new TextBlock(block.Render(variables), this._log)).ToList();
|
||||
: new TextBlock(((ITextRendering)block).Render(variables), this._log)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of blocks, render the Code Blocks, executing the functions and replacing placeholders with the functions result
|
||||
/// </summary>
|
||||
/// <param name="blocks">List of blocks, typically all the blocks found in a template</param>
|
||||
/// <param name="executionContext">Access into the current kernel execution context</param>
|
||||
/// <returns>An updated list of blocks where Code Blocks have rendered to Text Blocks</returns>
|
||||
/// <inheritdoc/>
|
||||
public async Task<IList<Block>> RenderCodeAsync(
|
||||
IList<Block> blocks,
|
||||
SKContext executionContext)
|
||||
@@ -132,127 +114,11 @@ public class PromptTemplateEngine : IPromptTemplateEngine
|
||||
}
|
||||
else
|
||||
{
|
||||
var codeResult = await block.RenderCodeAsync(executionContext);
|
||||
var codeResult = await ((ICodeRendering)block).RenderCodeAsync(executionContext);
|
||||
updatedBlocks.Add(new TextBlock(codeResult, this._log));
|
||||
}
|
||||
}
|
||||
|
||||
return updatedBlocks;
|
||||
}
|
||||
|
||||
#region private ================================================================================
|
||||
|
||||
private readonly ILogger _log;
|
||||
|
||||
// Blocks delimitation
|
||||
private const char Starter = '{';
|
||||
private const char Ender = '}';
|
||||
|
||||
private IList<Block> TokenizeInternal(string? template)
|
||||
{
|
||||
// An empty block consists of 4 chars: "{{}}"
|
||||
const int EMPTY_CODE_BLOCK_LENGTH = 4;
|
||||
// A block shorter than 5 chars is either empty or invalid, e.g. "{{ }}" and "{{$}}"
|
||||
const int MIN_CODE_BLOCK_LENGTH = EMPTY_CODE_BLOCK_LENGTH + 1;
|
||||
|
||||
// Render NULL to ""
|
||||
if (template == null)
|
||||
{
|
||||
return new List<Block> { new TextBlock("", this._log) };
|
||||
}
|
||||
|
||||
// If the template is "empty" return the content as a text block
|
||||
if (template.Length < MIN_CODE_BLOCK_LENGTH)
|
||||
{
|
||||
return new List<Block> { new TextBlock(template, this._log) };
|
||||
}
|
||||
|
||||
var blocks = new List<Block>();
|
||||
|
||||
var cursor = 0;
|
||||
var endOfLastBlock = 0;
|
||||
|
||||
var startPos = 0;
|
||||
var startFound = false;
|
||||
|
||||
while (cursor < template.Length - 1)
|
||||
{
|
||||
// When "{{" is found
|
||||
if (template[cursor] == Starter && template[cursor + 1] == Starter)
|
||||
{
|
||||
startPos = cursor;
|
||||
startFound = true;
|
||||
}
|
||||
// When "}}" is found
|
||||
else if (startFound && template[cursor] == Ender && template[cursor + 1] == Ender)
|
||||
{
|
||||
// If there is plain text between the current var/code block and the previous one, capture that as a TextBlock
|
||||
if (startPos > endOfLastBlock)
|
||||
{
|
||||
blocks.Add(new TextBlock(template, endOfLastBlock, startPos, this._log));
|
||||
}
|
||||
|
||||
// Skip ahead to the second "}" of "}}"
|
||||
cursor++;
|
||||
|
||||
// Extract raw block
|
||||
var contentWithDelimiters = SubStr(template, startPos, cursor + 1);
|
||||
|
||||
// Remove "{{" and "}}" delimiters and trim empty chars
|
||||
var contentWithoutDelimiters = contentWithDelimiters
|
||||
.Substring(2, contentWithDelimiters.Length - EMPTY_CODE_BLOCK_LENGTH).Trim();
|
||||
|
||||
if (contentWithoutDelimiters.Length == 0)
|
||||
{
|
||||
// If what is left is empty, consider the raw block a Text Block
|
||||
blocks.Add(new TextBlock(contentWithDelimiters, this._log));
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the block starts with "$" it's a variable
|
||||
if (VarBlock.HasVarPrefix(contentWithoutDelimiters))
|
||||
{
|
||||
// Note: validation is delayed to the time VarBlock is rendered
|
||||
blocks.Add(new VarBlock(contentWithoutDelimiters, this._log));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: validation is delayed to the time CodeBlock is rendered
|
||||
blocks.Add(new CodeBlock(contentWithoutDelimiters, this._log));
|
||||
}
|
||||
}
|
||||
|
||||
endOfLastBlock = cursor + 1;
|
||||
startFound = false;
|
||||
}
|
||||
|
||||
cursor++;
|
||||
}
|
||||
|
||||
// If there is something left after the last block, capture it as a TextBlock
|
||||
if (endOfLastBlock < template.Length)
|
||||
{
|
||||
blocks.Add(new TextBlock(template, endOfLastBlock, template.Length, this._log));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static string SubStr(string text, int startIndex, int stopIndex)
|
||||
{
|
||||
return text.Substring(startIndex, stopIndex - startIndex);
|
||||
}
|
||||
|
||||
private static void ValidateBlocksSyntax(IList<Block> blocks)
|
||||
{
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
if (!block.IsValid(out var error))
|
||||
{
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -25,15 +25,20 @@ public class TemplateException : Exception<TemplateException.ErrorCodes>
|
||||
/// </summary>
|
||||
SyntaxError = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The block type produced be the tokenizer was not expected
|
||||
/// </summary>
|
||||
UnexpectedBlockType = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The template requires an unknown function.
|
||||
/// </summary>
|
||||
FunctionNotFound = 1,
|
||||
FunctionNotFound = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The template execution failed, e.g. a function call threw an exception.
|
||||
/// </summary>
|
||||
RuntimeError = 2,
|
||||
RuntimeError = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel.TemplateEngine.Blocks;
|
||||
|
||||
namespace Microsoft.SemanticKernel.TemplateEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Simple tokenizer used for default SK template language.
|
||||
///
|
||||
/// BNF parsed by TemplateTokenizer:
|
||||
/// [template] ::= "" | [block] | [block] [template]
|
||||
/// [block] ::= [sk-block] | [text-block]
|
||||
/// [sk-block] ::= "{{" [variable] "}}" | "{{" [value] "}}" | "{{" [function-call] "}}"
|
||||
/// [text-block] ::= [any-char] | [any-char] [text-block]
|
||||
/// [any-char] ::= any char
|
||||
///
|
||||
/// BNF parsed by CodeTokenizer:
|
||||
/// [template] ::= "" | [variable] " " [template] | [value] " " [template] | [function-call] " " [template]
|
||||
/// [variable] ::= "$" [valid-name]
|
||||
/// [value] ::= "'" [text] "'" | '"' [text] '"'
|
||||
/// [function-call] ::= [function-id] | [function-id] [parameter]
|
||||
/// [parameter] ::= [variable] | [value]
|
||||
///
|
||||
/// BNF parsed by dedicated blocks
|
||||
/// [function-id] ::= [valid-name] | [valid-name] "." [valid-name]
|
||||
/// [valid-name] ::= [valid-symbol] | [valid-symbol] [valid-name]
|
||||
/// [valid-symbol] ::= [letter] | [digit] | "_"
|
||||
/// [letter] ::= "a" | "b" ... | "z" | "A" | "B" ... | "Z"
|
||||
/// [digit] ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
|
||||
/// </summary>
|
||||
internal class TemplateTokenizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new instance of SK tokenizer
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
public TemplateTokenizer(ILogger? log = null)
|
||||
{
|
||||
this._log = log ?? NullLogger.Instance;
|
||||
this._codeTokenizer = new CodeTokenizer(this._log);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract blocks from the given text
|
||||
/// </summary>
|
||||
/// <param name="text">Text to parse</param>
|
||||
/// <returns>List of blocks found in the text</returns>
|
||||
public IList<Block> Tokenize(string? text)
|
||||
{
|
||||
// An empty block consists of 4 chars: "{{}}"
|
||||
const int EMPTY_CODE_BLOCK_LENGTH = 4;
|
||||
// A block shorter than 5 chars is either empty or invalid, e.g. "{{ }}" and "{{$}}"
|
||||
const int MIN_CODE_BLOCK_LENGTH = EMPTY_CODE_BLOCK_LENGTH + 1;
|
||||
|
||||
// Render NULL to ""
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return new List<Block> { new TextBlock(string.Empty, this._log) };
|
||||
}
|
||||
|
||||
// If the template is "empty" return the content as a text block
|
||||
if (text.Length < MIN_CODE_BLOCK_LENGTH)
|
||||
{
|
||||
return new List<Block> { new TextBlock(text, this._log) };
|
||||
}
|
||||
|
||||
var blocks = new List<Block>();
|
||||
|
||||
var endOfLastBlock = 0;
|
||||
|
||||
var blockStartPos = 0;
|
||||
var blockStartFound = false;
|
||||
|
||||
var insideTextValue = false;
|
||||
var textValueDelimiter = '\0';
|
||||
|
||||
bool skipNextChar = false;
|
||||
char nextChar = text[0];
|
||||
for (int nextCharCursor = 1; nextCharCursor < text.Length; nextCharCursor++)
|
||||
{
|
||||
int currentCharPos = nextCharCursor - 1;
|
||||
int cursor = nextCharCursor;
|
||||
char currentChar = nextChar;
|
||||
nextChar = text[nextCharCursor];
|
||||
|
||||
if (skipNextChar)
|
||||
{
|
||||
skipNextChar = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// When "{{" is found outside a value
|
||||
// Note: "{{ {{x}}" => ["{{ ", "{{x}}"]
|
||||
if (!insideTextValue && currentChar == Symbols.BlockStarter && nextChar == Symbols.BlockStarter)
|
||||
{
|
||||
// A block starts at the first "{"
|
||||
blockStartPos = currentCharPos;
|
||||
blockStartFound = true;
|
||||
}
|
||||
|
||||
// After having found '{{'
|
||||
if (blockStartFound)
|
||||
{
|
||||
// While inside a text value, when the end quote is found
|
||||
if (insideTextValue)
|
||||
{
|
||||
if (currentChar == Symbols.EscapeChar && CanBeEscaped(nextChar))
|
||||
{
|
||||
skipNextChar = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentChar == textValueDelimiter)
|
||||
{
|
||||
insideTextValue = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// A value starts here
|
||||
if (IsQuote(currentChar))
|
||||
{
|
||||
insideTextValue = true;
|
||||
textValueDelimiter = currentChar;
|
||||
}
|
||||
// If the block ends here
|
||||
else if (currentChar == Symbols.BlockEnder && nextChar == Symbols.BlockEnder)
|
||||
{
|
||||
// If there is plain text between the current var/val/code block and the previous one, capture that as a TextBlock
|
||||
if (blockStartPos > endOfLastBlock)
|
||||
{
|
||||
blocks.Add(new TextBlock(text, endOfLastBlock, blockStartPos, this._log));
|
||||
}
|
||||
|
||||
// Extract raw block
|
||||
var contentWithDelimiters = SubStr(text, blockStartPos, cursor + 1);
|
||||
|
||||
// Remove "{{" and "}}" delimiters and trim empty chars
|
||||
var contentWithoutDelimiters = contentWithDelimiters
|
||||
.Substring(2, contentWithDelimiters.Length - EMPTY_CODE_BLOCK_LENGTH)
|
||||
.Trim();
|
||||
|
||||
if (contentWithoutDelimiters.Length == 0)
|
||||
{
|
||||
// If what is left is empty, consider the raw block a Text Block
|
||||
blocks.Add(new TextBlock(contentWithDelimiters, this._log));
|
||||
}
|
||||
else
|
||||
{
|
||||
List<Block> codeBlocks = this._codeTokenizer.Tokenize(contentWithoutDelimiters);
|
||||
|
||||
switch (codeBlocks[0].Type)
|
||||
{
|
||||
case BlockTypes.Variable:
|
||||
if (codeBlocks.Count > 1)
|
||||
{
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError,
|
||||
$"Invalid token detected after the variable: {contentWithoutDelimiters}");
|
||||
}
|
||||
|
||||
blocks.Add(codeBlocks[0]);
|
||||
break;
|
||||
|
||||
case BlockTypes.Value:
|
||||
if (codeBlocks.Count > 1)
|
||||
{
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError,
|
||||
$"Invalid token detected after the value: {contentWithoutDelimiters}");
|
||||
}
|
||||
|
||||
blocks.Add(codeBlocks[0]);
|
||||
break;
|
||||
|
||||
case BlockTypes.FunctionId:
|
||||
if (codeBlocks.Count > 2)
|
||||
{
|
||||
throw new TemplateException(TemplateException.ErrorCodes.SyntaxError,
|
||||
$"Functions support only one parameter: {contentWithoutDelimiters}");
|
||||
}
|
||||
|
||||
blocks.Add(new CodeBlock(codeBlocks, contentWithoutDelimiters, this._log));
|
||||
break;
|
||||
|
||||
case BlockTypes.Code:
|
||||
case BlockTypes.Text:
|
||||
case BlockTypes.Undefined:
|
||||
default:
|
||||
throw new TemplateException(TemplateException.ErrorCodes.UnexpectedBlockType,
|
||||
$"Code tokenizer returned an incorrect first token type {codeBlocks[0].Type:G}");
|
||||
}
|
||||
}
|
||||
|
||||
endOfLastBlock = cursor + 1;
|
||||
blockStartFound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is something left after the last block, capture it as a TextBlock
|
||||
if (endOfLastBlock < text.Length)
|
||||
{
|
||||
blocks.Add(new TextBlock(text, endOfLastBlock, text.Length, this._log));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
#region private ================================================================================
|
||||
|
||||
private readonly ILogger _log;
|
||||
private readonly CodeTokenizer _codeTokenizer;
|
||||
|
||||
private static string SubStr(string text, int startIndex, int stopIndex)
|
||||
{
|
||||
return text.Substring(startIndex, stopIndex - startIndex);
|
||||
}
|
||||
|
||||
private static bool IsQuote(char c)
|
||||
{
|
||||
return c is Symbols.DblQuote or Symbols.SglQuote;
|
||||
}
|
||||
|
||||
private static bool CanBeEscaped(char c)
|
||||
{
|
||||
return c is Symbols.DblQuote or Symbols.SglQuote or Symbols.EscapeChar;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace KernelHttpServer.Config;
|
||||
|
||||
internal class Constants
|
||||
{
|
||||
public class SKHttpHeaders
|
||||
|
||||
@@ -23,7 +23,6 @@ public class SemanticKernelEndpoint
|
||||
this._memoryStore = memoryStore;
|
||||
}
|
||||
|
||||
|
||||
[Function("InvokeFunction")]
|
||||
public async Task<HttpResponseData> InvokeFunctionAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "skills/{skillName}/invoke/{functionName}")]
|
||||
|
||||
@@ -70,8 +70,8 @@ public static class Example15_MemorySkill
|
||||
const string RECALL_FUNCTION_DEFINITION = @"
|
||||
Consider only the facts below when answering questions.
|
||||
|
||||
About me: {{recall $fact1}}
|
||||
About me: {{recall $fact2}}
|
||||
About me: {{recall 'where did I grow up?'}}
|
||||
About me: {{recall 'where do I live?'}}
|
||||
|
||||
Question: {{$query}}
|
||||
|
||||
@@ -80,8 +80,6 @@ Answer:
|
||||
|
||||
var aboutMeOracle = kernel.CreateSemanticFunction(RECALL_FUNCTION_DEFINITION, maxTokens: 100);
|
||||
|
||||
context["fact1"] = "where did I grow up?";
|
||||
context["fact2"] = "where do I live?";
|
||||
context["query"] = "Do I live in the same town where I grew up?";
|
||||
context[TextMemorySkill.RelevanceParam] = "0.8";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user