Skip to content

Commit

Permalink
.Net: Handlebars planner complex types (microsoft#3502)
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.
-->

Support for complex types in Handlebars Planner

### Description

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

Native Plugin with Complex Types

![image](https://github.com/microsoft/semantic-kernel/assets/125500434/01c9d5a0-b98b-4d88-ae5a-f38425a2d1ba)

Remote Plugin with Complex Types

![image](https://github.com/microsoft/semantic-kernel/assets/125500434/ebd3afb7-1c49-483a-b4ea-6f41783837bf)


### 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
teresaqhoang authored Nov 22, 2023
1 parent 5623544 commit cbf31ee
Show file tree
Hide file tree
Showing 19 changed files with 1,278 additions and 452 deletions.
142 changes: 86 additions & 56 deletions dotnet/samples/KernelSyntaxExamples/Example65_HandlebarsPlanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,47 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions;
using Microsoft.SemanticKernel.Planning.Handlebars;
using Plugins.DictionaryPlugin;
using RepoUtils;

/**
* This example shows how to use the Handlebars sequential planner.
*/
public static class Example65_HandlebarsPlanner
{
private static int s_sampleCount;
private static int s_sampleIndex;

private const string CourseraPluginName = "CourseraPlugin";

/// <summary>
/// Show how to create a plan with Handlebars and execute it.
/// </summary>
public static async Task RunAsync()
{
s_sampleCount = 0;
Console.WriteLine($"======== {nameof(Example65_HandlebarsPlanner)} ========");
s_sampleIndex = 1;
bool shouldPrintPrompt = true;

// Using primitive types as inputs and outputs
await PlanNotPossibleSampleAsync();
await RunDictionarySampleAsync();
await RunDictionaryWithBasicTypesSampleAsync();
await RunPoetrySampleAsync();
await RunBookSampleAsync();

// Using Complex Types as inputs and outputs
await RunLocalDictionaryWithComplexTypesSampleAsync(shouldPrintPrompt);
}

private static void WriteSampleHeadingToConsole(string name)
{
Console.WriteLine($"======== [Handlebars Planner] Sample {s_sampleCount++} - Create and Execute {name} Plan ========");
Console.WriteLine($"======== [Handlebars Planner] Sample {s_sampleIndex++} - Create and Execute {name} Plan ========");
}

private static async Task RunSampleAsync(string goal, params string[] pluginDirectoryNames)
private static async Task RunSampleAsync(string goal, bool shouldPrintPrompt = false, params string[] pluginDirectoryNames)
{
string apiKey = TestConfiguration.AzureOpenAI.ApiKey;
string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName;
Expand All @@ -58,9 +64,20 @@ private static async Task RunSampleAsync(string goal, params string[] pluginDire
apiKey: apiKey)
.Build();

if (pluginDirectoryNames[0] == DictionaryPlugin.PluginName)
if (pluginDirectoryNames[0] == StringParamsDictionaryPlugin.PluginName)
{
kernel.ImportPluginFromObject(new DictionaryPlugin(), DictionaryPlugin.PluginName);
kernel.ImportPluginFromObject(new StringParamsDictionaryPlugin(), StringParamsDictionaryPlugin.PluginName);
}
else if (pluginDirectoryNames[0] == ComplexParamsDictionaryPlugin.PluginName)
{
kernel.ImportPluginFromObject(new ComplexParamsDictionaryPlugin(), ComplexParamsDictionaryPlugin.PluginName);
}
else if (pluginDirectoryNames[0] == CourseraPluginName)
{
await kernel.ImportPluginFromOpenApiAsync(
CourseraPluginName,
new Uri("https://www.coursera.org/api/rest/v1/search/openapi.yaml")
);
}
else
{
Expand All @@ -72,29 +89,43 @@ private static async Task RunSampleAsync(string goal, params string[] pluginDire
}
}

// The gpt-35-turbo model does not handle loops well in the plans.
var allowLoopsInPlan = chatDeploymentName.Contains("gpt-35-turbo", StringComparison.OrdinalIgnoreCase) ? false : true;
// Use gpt-4 or newer models if you want to test with loops.
// Older models like gpt-35-turbo are less recommended. They do handle loops but are more prone to syntax errors.
var allowLoopsInPlan = chatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase);
var planner = new HandlebarsPlanner(
kernel,
new HandlebarsPlannerConfig()
{
// Change this if you want to test with loops regardless of model selection.
AllowLoops = allowLoopsInPlan
});

var planner = new HandlebarsPlanner(kernel, new HandlebarsPlannerConfig() { AllowLoops = allowLoopsInPlan });
Console.WriteLine($"Goal: {goal}");

// Create the plan
var plan = await planner.CreatePlanAsync(goal);

if (shouldPrintPrompt)
{
// Print the prompt template
Console.WriteLine($"\nPrompt template:\n{plan.Prompt}");
}

Console.WriteLine($"\nOriginal plan:\n{plan}");

// Execute the plan
var result = plan.Invoke(kernel.CreateNewContext(), new Dictionary<string, object?>(), CancellationToken.None);
Console.WriteLine($"\nResult:\n{result.GetValue<string>()}\n");
}

private static async Task PlanNotPossibleSampleAsync()
private static async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeadingToConsole("Plan Not Possible");

try
{
// Load additional plugins to enable planner but not enough for the given goal.
await RunSampleAsync("Send Mary an email with the list of meetings I have scheduled today.", "SummarizePlugin");
await RunSampleAsync("Send Mary an email with the list of meetings I have scheduled today.", shouldPrintPrompt, "SummarizePlugin");
}
catch (SKException e)
{
Expand All @@ -111,10 +142,10 @@ Additional helpers may be required.
}
}

private static async Task RunDictionarySampleAsync()
private static async Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeadingToConsole("Dictionary");
await RunSampleAsync("Get a random word and its definition.", DictionaryPlugin.PluginName);
await RunSampleAsync("Get a random word and its definition.", shouldPrintPrompt, StringParamsDictionaryPlugin.PluginName);
/*
Original plan:
{{!-- Step 1: Get a random word --}}
Expand All @@ -131,10 +162,44 @@ private static async Task RunDictionarySampleAsync()
*/
}

private static async Task RunPoetrySampleAsync()
private static async Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeadingToConsole("Complex Types with Local Dictionary Plugin");
await RunSampleAsync("Teach me two random words and their definition.", shouldPrintPrompt, ComplexParamsDictionaryPlugin.PluginName);
/*
Original Plan:
{{!-- Step 1: Get two random dictionary entries --}}
{{set "entry1" (DictionaryPlugin-GetRandomEntry)}}
{{set "entry2" (DictionaryPlugin-GetRandomEntry)}}
{{!-- Step 2: Extract words from the entries --}}
{{set "word1" (DictionaryPlugin-GetWord entry=(get "entry1"))}}
{{set "word2" (DictionaryPlugin-GetWord entry=(get "entry2"))}}
{{!-- Step 3: Extract definitions for the words --}}
{{set "definition1" (DictionaryPlugin-GetDefinition word=(get "word1"))}}
{{set "definition2" (DictionaryPlugin-GetDefinition word=(get "word2"))}}
{{!-- Step 4: Display the words and their definitions --}}
Word 1: {{json (get "word1")}}
Definition: {{json (get "definition1")}}
Word 2: {{json (get "word2")}}
Definition: {{json (get "definition2")}}
Result:
Word 1: apple
Definition 1: a round fruit with red, green, or yellow skin and a white flesh
Word 2: dog
Definition 2: a domesticated animal with four legs, a tail, and a keen sense of smell that is often used for hunting or companionship
*/
}

private static async Task RunPoetrySampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeadingToConsole("Poetry");
await RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", "SummarizePlugin", "WriterPlugin");
await RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", shouldPrintPrompt, "SummarizePlugin", "WriterPlugin");
/*
Original plan:
{{!-- Step 1: Initialize the scenario for the poem --}}
Expand All @@ -158,10 +223,10 @@ Al mistero che lo faceva brillare.
*/
}

private static async Task RunBookSampleAsync()
private static async Task RunBookSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeadingToConsole("Book Creation");
await RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", "WriterPlugin", "MiscPlugin");
await RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", shouldPrintPrompt, "WriterPlugin", "MiscPlugin");
/*
Original plan:
{{!-- Step 1: Initialize the book title and chapter count --}}
Expand All @@ -185,39 +250,4 @@ private static async Task RunBookSampleAsync()
{{/each}}
*/
}

/// <summary>
/// Plugin example with two native functions, where one function gets a random word and the other returns a definition for a given word.
/// </summary>
private sealed class DictionaryPlugin
{
public const string PluginName = nameof(DictionaryPlugin);

private readonly Dictionary<string, string> _dictionary = new()
{
{"apple", "a round fruit with red, green, or yellow skin and a white flesh"},
{"book", "a set of printed or written pages bound together along one edge"},
{"cat", "a small furry animal with whiskers and a long tail that is often kept as a pet"},
{"dog", "a domesticated animal with four legs, a tail, and a keen sense of smell that is often used for hunting or companionship"},
{"elephant", "a large gray mammal with a long trunk, tusks, and ears that lives in Africa and Asia"}
};

[SKFunction, SKName("GetRandomWord"), System.ComponentModel.Description("Gets a random word from a dictionary of common words and their definitions.")]
public string GetRandomWord()
{
// Get random number
var index = RandomNumberGenerator.GetInt32(0, this._dictionary.Count - 1);

// Return the word at the random index
return this._dictionary.ElementAt(index).Key;
}

[SKFunction, SKName("GetDefinition"), System.ComponentModel.Description("Gets the definition for a given word.")]
public string GetDefinition([System.ComponentModel.Description("Word to get definition for.")] string word)
{
return this._dictionary.TryGetValue(word, out var definition)
? definition
: "Word not found";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.SemanticKernel;

namespace Plugins.DictionaryPlugin;

/// <summary>
/// Plugin example with two Local functions, where one function gets a random word and the other returns a definition for a given word.
/// </summary>
public sealed class ComplexParamsDictionaryPlugin
{
public const string PluginName = nameof(ComplexParamsDictionaryPlugin);

private readonly List<DictionaryEntry> _dictionary = new()
{
new DictionaryEntry("apple", "a round fruit with red, green, or yellow skin and a white flesh"),
new DictionaryEntry("book", "a set of printed or written pages bound together along one edge"),
new DictionaryEntry("cat", "a small furry animal with whiskers and a long tail that is often kept as a pet"),
new DictionaryEntry("dog", "a domesticated animal with four legs, a tail, and a keen sense of smell that is often used for hunting or companionship"),
new DictionaryEntry("elephant", "a large gray mammal with a long trunk, tusks, and ears that lives in Africa and Asia")
};

[SKFunction, SKName("GetRandomEntry"), System.ComponentModel.Description("Gets a random word from a dictionary of common words and their definitions.")]
public DictionaryEntry GetRandomEntry()
{
// Get random number
var index = RandomNumberGenerator.GetInt32(0, this._dictionary.Count - 1);

// Return the word at the random index
return this._dictionary[index];
}

[SKFunction, SKName("GetWord"), System.ComponentModel.Description("Gets the word for a given dictionary entry.")]
public string GetWord([System.ComponentModel.Description("Word to get definition for.")] DictionaryEntry entry)
{
// Return the definition or a default message
return this._dictionary.FirstOrDefault(e => e.Word == entry.Word)?.Word ?? "Entry not found";
}

[SKFunction, SKName("GetDefinition"), System.ComponentModel.Description("Gets the definition for a given word.")]
public string GetDefinition([System.ComponentModel.Description("Word to get definition for.")] string word)
{
// Return the definition or a default message
return this._dictionary.FirstOrDefault(e => e.Word == word)?.Definition ?? "Word not found";
}
}

/// <summary>
/// In order to use custom types, <see cref="TypeConverter"/> should be specified,
/// that will convert object instance to string representation.
/// </summary>
/// <remarks>
/// <see cref="TypeConverter"/> is used to represent complex object as meaningful string, so
/// it can be passed to AI for further processing using semantic functions.
/// It's possible to choose any format (e.g. XML, JSON, YAML) to represent your object.
/// </remarks>
[TypeConverter(typeof(DictionaryEntryConverter))]
public sealed class DictionaryEntry
{
public string Word { get; set; } = string.Empty;
public string Definition { get; set; } = string.Empty;

public DictionaryEntry(string word, string definition)
{
this.Word = word;
this.Definition = definition;
}
}

/// <summary>
/// Implementation of <see cref="TypeConverter"/> for <see cref="DictionaryEntry"/>.
/// In this example, object instance is serialized with <see cref="JsonSerializer"/> from System.Text.Json,
/// but it's possible to convert object to string using any other serialization logic.
/// </summary>
#pragma warning disable CA1812 // instantiated by Kernel
public sealed class DictionaryEntryConverter : TypeConverter
#pragma warning restore CA1812
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => true;

/// <summary>
/// This method is used to convert object from string to actual type. This will allow to pass object to
/// Local function which requires it.
/// </summary>
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return JsonSerializer.Deserialize<DictionaryEntry>((string)value);
}

/// <summary>
/// This method is used to convert actual type to string representation, so it can be passed to AI
/// for further processing.
/// </summary>
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
return JsonSerializer.Serialize(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using Microsoft.SemanticKernel;

namespace Plugins.DictionaryPlugin;

/// <summary>
/// Plugin example with two native functions, where one function gets a random word and the other returns a definition for a given word.
/// </summary>
public sealed class StringParamsDictionaryPlugin
{
public const string PluginName = nameof(StringParamsDictionaryPlugin);

private readonly Dictionary<string, string> _dictionary = new()
{
{"apple", "a round fruit with red, green, or yellow skin and a white flesh"},
{"book", "a set of printed or written pages bound together along one edge"},
{"cat", "a small furry animal with whiskers and a long tail that is often kept as a pet"},
{"dog", "a domesticated animal with four legs, a tail, and a keen sense of smell that is often used for hunting or companionship"},
{"elephant", "a large gray mammal with a long trunk, tusks, and ears that lives in Africa and Asia"}
};

[SKFunction, SKName("GetRandomWord"), System.ComponentModel.Description("Gets a random word from a dictionary of common words and their definitions.")]
public string GetRandomWord()
{
// Get random number
var index = RandomNumberGenerator.GetInt32(0, this._dictionary.Count - 1);

// Return the word at the random index
return this._dictionary.ElementAt(index).Key;
}

[SKFunction, SKName("GetDefinition"), System.ComponentModel.Description("Gets the definition for a given word.")]
public string GetDefinition([System.ComponentModel.Description("Word to get definition for.")] string word)
{
return this._dictionary.TryGetValue(word, out var definition)
? definition
: "Word not found";
}
}
Loading

0 comments on commit cbf31ee

Please sign in to comment.