Fixing logic in custom or HB helper (#4804)

### 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.
-->

The custom `or` helper had a bug where it was checking for null values,
rather than checking for non-null values. This PR changes the condition
to `not null`.

Also, renamed a heading in the CreatePlan prompt to better guide the
model in identifying Handlebars built-in _block_ helpers.

### Description

Additional changes to the `GetArgumentValue` method in
`KernelHelperUtils.cs` to return null if variable isn't defined.

Before, if the template referenced a variable that wasn't defined in
`KernelArguments`, the method would just return the key as a string.
Now, it just returns null (as expected).
> I had it returning a string before because I thought Handlebars would
default the argument to its string representation since it templates
regular text, but I double checked [HB
docs](https://handlebarsjs.com/guide/expressions.html#helpers-with-multiple-parameters),
and generally, string literals are surrounded by quotes when a raw value
is passed as an argument.

Should resolve these issues:
- https://github.com/microsoft/semantic-kernel/issues/4661
- https://github.com/microsoft/semantic-kernel/issues/4732

### 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 😄

---------

Co-authored-by: Gina Triolo <[email protected]>
This commit is contained in:
Teresa Hoang
2024-02-01 20:45:14 +00:00
committed by GitHub
co-authored by Gina Triolo
parent d9800fe7f7
commit 52e317a796
4 changed files with 71 additions and 7 deletions
@@ -41,7 +41,7 @@ internal static class KernelHelpersUtils
// Thus, we try to get the value from the kernel arguments dictionary.
if (argument is UndefinedBindingResult result)
{
return kernelArguments.TryGetValue(result.Value, out var variable) ? variable : result.Value;
return kernelArguments.TryGetValue(result.Value, out var variable) ? variable : null;
}
return argument;
@@ -136,7 +136,7 @@ internal static class KernelSystemHelpers
return arg switch
{
bool booleanArg => booleanArg,
_ => arg is null
_ => arg is not null
};
});
});
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
@@ -30,7 +31,7 @@ public sealed class HandlebarsPlannerTests : IDisposable
[Theory]
[InlineData(true, "Write a joke and send it in an e-mail to Kai.", "SendEmail", "test")]
public async Task CreatePlanFunctionFlowAsync(bool useChatModel, string prompt, string expectedFunction, string expectedPlugin)
public async Task CreatePlanFunctionFlowAsync(bool useChatModel, string goal, string expectedFunction, string expectedPlugin)
{
// Arrange
bool useEmbeddings = false;
@@ -39,7 +40,7 @@ public sealed class HandlebarsPlannerTests : IDisposable
TestHelpers.ImportSamplePlugins(kernel, "FunPlugin");
// Act
var plan = await new HandlebarsPlanner().CreatePlanAsync(kernel, prompt);
var plan = await new HandlebarsPlanner().CreatePlanAsync(kernel, goal);
// Assert expected function
Assert.Contains(
@@ -51,14 +52,14 @@ public sealed class HandlebarsPlannerTests : IDisposable
[RetryTheory]
[InlineData("Write a novel about software development that is 3 chapters long.", "NovelChapter", "WriterPlugin")]
public async Task CreatePlanWithDefaultsAsync(string prompt, string expectedFunction, string expectedPlugin)
public async Task CreatePlanWithDefaultsAsync(string goal, string expectedFunction, string expectedPlugin)
{
// Arrange
Kernel kernel = this.InitializeKernel();
TestHelpers.ImportSamplePlugins(kernel, "WriterPlugin", "MiscPlugin");
// Act
var plan = await new HandlebarsPlanner().CreatePlanAsync(kernel, prompt);
var plan = await new HandlebarsPlanner().CreatePlanAsync(kernel, goal);
// Assert
Assert.Contains(
@@ -68,6 +69,51 @@ public sealed class HandlebarsPlannerTests : IDisposable
);
}
[Theory]
[InlineData(true, "List each property of the default Qux object.", "## Complex types", @"### Qux:
{
""type"": ""Object"",
""properties"": {
""Bar"": {
""type"": ""String"",
},
""Baz"": {
""type"": ""Int32"",
},
}
}", "GetDefaultQux", "Foo")]
public async Task CreatePlanWithComplexTypesDefinitionsAsync(bool useChatModel, string goal, string expectedSectionHeader, string expectedTypeHeader, string expectedFunction, string expectedPlugin)
{
// Arrange
bool useEmbeddings = false;
var kernel = this.InitializeKernel(useEmbeddings, useChatModel);
kernel.ImportPluginFromObject(new Foo());
// Act
var plan = await new HandlebarsPlanner().CreatePlanAsync(kernel, goal);
// Assert expected section header for Complex Types in prompt
Assert.Contains(
expectedSectionHeader,
plan.Prompt,
StringComparison.CurrentCulture
);
// Assert expected complex parameter type in prompt
Assert.Contains(
expectedTypeHeader,
plan.Prompt,
StringComparison.CurrentCulture
);
// Assert expected function in plan
Assert.Contains(
$"{expectedPlugin}-{expectedFunction}",
plan.ToString(),
StringComparison.CurrentCulture
);
}
private Kernel InitializeKernel(bool useEmbeddings = false, bool useChatModel = true)
{
AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get<AzureOpenAIConfiguration>();
@@ -110,6 +156,24 @@ public sealed class HandlebarsPlannerTests : IDisposable
private readonly RedirectOutput _testOutputHelper;
private readonly IConfigurationRoot _configuration;
private sealed class Foo
{
public sealed class Qux
{
public string Bar { get; set; } = string.Empty;
public int Baz { get; set; }
public Qux(string bar, int baz)
{
this.Bar = bar;
this.Baz = baz;
}
}
[KernelFunction, Description("Returns default Qux object.")]
public Qux GetDefaultQux() => new("bar", 42);
}
public void Dispose()
{
this._testOutputHelper.Dispose();
@@ -72,7 +72,7 @@ What's the sum of 5+10+15?
{{#message role="user"}}
The following helpers are available to you:
## Out-of-the-box helpers
## Built-in block helpers
- `\{{#if}}\{{/if}}`
- `\{{#unless}}\{{/unless}}`{{#if allowLoops}}
- `\{{#each}}\{{/each}}`{{/if}}