Skip to content

Commit

Permalink
.Net: Handlebars prompt template support (microsoft#3392)
Browse files Browse the repository at this point in the history
### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
  • Loading branch information
markwallace-microsoft authored Nov 8, 2023
1 parent cc6dd60 commit 86a2e3f
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 3 deletions.
7 changes: 4 additions & 3 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageVersion Include="Azure.AI.OpenAI" Version="1.0.0-beta.8" />
<PackageVersion Include="Azure.Identity" Version="1.10.3" />
<PackageVersion Include="Azure.Search.Documents" Version="11.5.0-beta.5" />
<PackageVersion Include="Handlebars.Net" Version="2.1.4" />
<PackageVersion Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />
<PackageVersion Include="Microsoft.Azure.Kusto.Data" Version="11.3.5" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="[1.1.0, )" />
Expand Down Expand Up @@ -61,9 +62,9 @@
<PackageVersion Include="CoreCLR-NCalc" Version="2.2.113" />
<PackageVersion Include="YamlDotNet" Version="13.7.1" />
<!-- Memory stores -->
<PackageVersion Include="Pgvector" Version="0.1.4"/>
<PackageVersion Include="NRedisStack" Version="0.9.0"/>
<PackageVersion Include="Milvus.Client" Version="2.2.2-preview.6"/>
<PackageVersion Include="Pgvector" Version="0.1.4" />
<PackageVersion Include="NRedisStack" Version="0.9.0" />
<PackageVersion Include="Milvus.Client" Version="2.2.2-preview.6" />
<!-- Symbols -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<!-- Analyzers -->
Expand Down
8 changes: 8 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Milvus",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.Memory", "src\Plugins\Plugins.Memory\Plugins.Memory.csproj", "{E91365A1-8B01-4AB8-825F-67E3515EADCD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemplateEngine.Handlebars", "src\Extensions\TemplateEngine.Handlebars\TemplateEngine.Handlebars.csproj", "{4CA6B046-DFE1-417C-A4A0-43BFD262C681}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "experimental", "experimental", "{A2357CF8-3BB9-45A1-93F1-B366C9B63658}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.Flow", "src\Experimental\Orchestration.Flow\Experimental.Orchestration.Flow.csproj", "{C8BFFC74-3050-4F63-9E9D-C4F600427DE9}"
Expand Down Expand Up @@ -393,6 +394,12 @@ Global
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Publish|Any CPU.Build.0 = Debug|Any CPU
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Release|Any CPU.Build.0 = Release|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Publish|Any CPU.Build.0 = Publish|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Release|Any CPU.Build.0 = Release|Any CPU
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
Expand Down Expand Up @@ -467,6 +474,7 @@ Global
{CC77DCFA-A419-4202-A98A-868CDF457792} = {A21FAC7C-0C09-4EAD-843B-926ACEF73C80}
{8B754E80-7A97-4585-8D7E-1D588FA5F727} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{E91365A1-8B01-4AB8-825F-67E3515EADCD} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132}
{4CA6B046-DFE1-417C-A4A0-43BFD262C681} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
{A2357CF8-3BB9-45A1-93F1-B366C9B63658} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{CB6E74CD-3A25-459F-A578-DEF25A414335} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.TemplateEngine;
using Microsoft.SemanticKernel.TemplateEngine.Basic;
using Microsoft.SemanticKernel.TemplateEngine.Handlebars;
using RepoUtils;

/**
* This example shows how to use multiple prompt template formats.
*/
// ReSharper disable once InconsistentNaming
public static class Example64_MultiplePromptTemplates
{
/// <summary>
/// Show how to combine multiple prompt template factories.
/// </summary>
public static async Task RunAsync()
{
Console.WriteLine("======== Example64_MultiplePromptTemplates ========");

string apiKey = TestConfiguration.AzureOpenAI.ApiKey;
string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName;
string endpoint = TestConfiguration.AzureOpenAI.Endpoint;

if (apiKey == null || chatDeploymentName == null || endpoint == null)
{
Console.WriteLine("Azure endpoint, apiKey, or deploymentName not found. Skipping example.");
return;
}

IKernel kernel = new KernelBuilder()
.WithLoggerFactory(ConsoleLogger.LoggerFactory)
.WithAzureOpenAIChatCompletionService(
deploymentName: chatDeploymentName,
endpoint: endpoint,
serviceId: "AzureOpenAIChat",
apiKey: apiKey)
.Build();

var promptTemplateFactory = new AggregatorPromptTemplateFactory(
new BasicPromptTemplateFactory(),
new HandlebarsPromptTemplateFactory());

var skPrompt = "Hello AI, my name is {{$name}}. What is the origin of my name?";
var handlebarsPrompt = "Hello AI, my name is {{name}}. What is the origin of my name?";

await RunSemanticFunctionAsync(kernel, skPrompt, "semantic-kernel", promptTemplateFactory);
await RunSemanticFunctionAsync(kernel, handlebarsPrompt, "handlebars", promptTemplateFactory);
}

public static async Task RunSemanticFunctionAsync(IKernel kernel, string prompt, string templateFormat, IPromptTemplateFactory promptTemplateFactory)
{
Console.WriteLine($"======== {templateFormat} : {prompt} ========");

var skfunction = kernel.CreateSemanticFunction(
promptTemplate: prompt,
functionName: "MyFunction",
promptTemplateConfig: new PromptTemplateConfig()
{
TemplateFormat = templateFormat
},
promptTemplateFactory: promptTemplateFactory
);

var variables = new ContextVariables()
{
{ "name", "Bob" }
};

var result = await kernel.RunAsync(skfunction, variables);
Console.WriteLine(result.GetValue<string>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<ProjectReference Include="..\..\src\Extensions\Reliability.Basic\Reliability.Basic.csproj" />
<ProjectReference Include="..\..\src\Extensions\TemplateEngine.Basic\TemplateEngine.Basic.csproj" />
<ProjectReference Include="..\..\src\Extensions\Reliability.Polly\Reliability.Polly.csproj" />
<ProjectReference Include="..\..\src\Extensions\TemplateEngine.Handlebars\TemplateEngine.Handlebars.csproj" />
<ProjectReference Include="..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Plugins\Plugins.Core\Plugins.Core.csproj" />
<ProjectReference Include="..\..\src\Plugins\Plugins.Memory\Plugins.Memory.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@
<ProjectReference Include="..\Reliability.Basic\Reliability.Basic.csproj" />
<ProjectReference Include="..\TemplateEngine.Basic\TemplateEngine.Basic.csproj" />
<ProjectReference Include="..\Reliability.Polly\Reliability.Polly.csproj" />
<ProjectReference Include="..\TemplateEngine.Handlebars\TemplateEngine.Handlebars.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.TemplateEngine;
using Microsoft.SemanticKernel.TemplateEngine.Handlebars;
using Xunit;

namespace SemanticKernel.Extensions.UnitTests.TemplateEngine.Handlebars;

public sealed class HandlebarsPromptTemplateFactoryTests
{
[Fact]
public void ItCreatesHandlebarsPromptTemplate()
{
// Arrange
var templateString = "{{input}}";
var target = new HandlebarsPromptTemplateFactory();

// Act
var result = target.Create(templateString, new PromptTemplateConfig() { TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat });

// Assert
Assert.NotNull(result);
Assert.True(result is HandlebarsPromptTemplate);
}

[Fact]
public void ItThrowsExceptionForUnknowPromptTemplateFormat()
{
// Arrange
var templateString = "{{input}}";
var target = new HandlebarsPromptTemplateFactory();

// Act
// Assert
Assert.Throws<SKException>(() => target.Create(templateString, new PromptTemplateConfig() { TemplateFormat = "unknown-format" }));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Services;
using Microsoft.SemanticKernel.TemplateEngine;
using Microsoft.SemanticKernel.TemplateEngine.Handlebars;
using Moq;
using SemanticKernel.Extensions.UnitTests.XunitHelpers;
using Xunit;
using Xunit.Abstractions;
using static Microsoft.SemanticKernel.TemplateEngine.PromptTemplateConfig;

namespace SemanticKernel.Extensions.UnitTests.TemplateEngine.Handlebars;

public sealed class HandlebarsPromptTemplateTests
{
private readonly HandlebarsPromptTemplateFactory _factory;
private readonly IKernel _kernel;
private readonly ContextVariables _variables;
private readonly Mock<IReadOnlyFunctionCollection> _functions;
private readonly ITestOutputHelper _logger;
private readonly Mock<IFunctionRunner> _functionRunner;
private readonly Mock<IAIServiceProvider> _serviceProvider;
private readonly Mock<IAIServiceSelector> _serviceSelector;

public HandlebarsPromptTemplateTests(ITestOutputHelper testOutputHelper)
{
this._logger = testOutputHelper;
this._factory = new HandlebarsPromptTemplateFactory(TestConsoleLogger.LoggerFactory);
this._kernel = new KernelBuilder().Build();
this._variables = new ContextVariables(Guid.NewGuid().ToString("X"));

this._functions = new Mock<IReadOnlyFunctionCollection>();
this._functionRunner = new Mock<IFunctionRunner>();
this._serviceProvider = new Mock<IAIServiceProvider>();
this._serviceSelector = new Mock<IAIServiceSelector>();
}

[Fact]
public async Task ItRendersVariablesAsync()
{
// Arrange
this._variables.Set("bar", "Bar");
var template = "Foo {{bar}}";
var target = (HandlebarsPromptTemplate)this._factory.Create(template, new PromptTemplateConfig() { TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat });
var context = this._kernel.CreateNewContext(this._variables);

// Act
var prompt = await target.RenderAsync(context);

// Assert
Assert.Equal("Foo Bar", prompt);
}

[Fact]
public async Task ItRendersFunctionsAsync()
{
// Arrange
this._kernel.ImportFunctions(new Foo(), "Foo");
var template = "Foo {{Foo_Bar}}";
var target = (HandlebarsPromptTemplate)this._factory.Create(template, new PromptTemplateConfig() { TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat });
var context = this._kernel.CreateNewContext(this._variables);

// Act
var prompt = await target.RenderAsync(context);

// Assert
Assert.Equal("Foo Bar", prompt);
}

[Fact]
public async Task ItRendersAsyncFunctionsAsync()
{
// Arrange
this._kernel.ImportFunctions(new Foo(), "Foo");
var template = "Foo {{Foo_Bar}} {{Foo_Baz}}";
var target = (HandlebarsPromptTemplate)this._factory.Create(template, new PromptTemplateConfig() { TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat });
var context = this._kernel.CreateNewContext(this._variables);

// Act
var prompt = await target.RenderAsync(context);

// Assert
Assert.Equal("Foo Bar Baz", prompt);
}

[Fact]
public void ItReturnsParameters()
{
// Arrange
var promptTemplateConfig = new PromptTemplateConfig()
{
TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat
};
promptTemplateConfig.Input.Parameters.Add(new InputParameter()
{
Name = "bar",
Description = "Bar",
DefaultValue = "Bar"
});
promptTemplateConfig.Input.Parameters.Add(new InputParameter()
{
Name = "baz",
Description = "Baz",
DefaultValue = "Baz"
});
var template = "Foo {{Bar}} {{Baz}}";
var target = (HandlebarsPromptTemplate)this._factory.Create(template, promptTemplateConfig);

// Act
var parameters = target.Parameters;

// Assert
Assert.Equal(2, parameters.Count);
}

[Fact]
public async Task ItUsesDefaultValuesAsync()
{
// Arrange
var promptTemplateConfig = new PromptTemplateConfig()
{
TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat
};
promptTemplateConfig.Input.Parameters.Add(new InputParameter()
{
Name = "Bar",
Description = "Bar",
DefaultValue = "Bar"
});
promptTemplateConfig.Input.Parameters.Add(new InputParameter()
{
Name = "Baz",
Description = "Baz",
DefaultValue = "Baz"
});
var template = "Foo {{Bar}} {{Baz}}";
var target = (HandlebarsPromptTemplate)this._factory.Create(template, promptTemplateConfig);
var context = this._kernel.CreateNewContext(this._variables);

// Act
var prompt = await target.RenderAsync(context);

// Assert
Assert.Equal("Foo Bar Baz", prompt);
}

private sealed class Foo
{
[SKFunction, Description("Return Bar")]
public string Bar() => "Bar";

[SKFunction, Description("Return Baz")]
public async Task<string> BazAsync()
{
await Task.Delay(1000);
return await Task.FromResult("Baz");
}
}
}
Loading

0 comments on commit 86a2e3f

Please sign in to comment.