Files
semantic-kernel/dotnet/samples/KernelSyntaxExamples/Example65_HandlebarsPlanner.cs
T
Teresa HoangandTao Chen 6652ce61c2 .Net: Configurable Handlebars Planner prompt (#4918)
### 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.
-->
Addresses issues:
- #4067 

This PR adds more flexibility to the HandlebarsPlanner by:
- Splitting the `CreatePlanPrompt` into well-defined partials. Each
partial maps to an essential section within the prompt.
- Opening options to provide more context when invoking the planner.

Not in scope of this PR:
- Option to override CreatePlan prompt completely

### Description

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

As the `CreatePlan` prompt has been broken down into Partials, users
have the ability to override these partials (any or all) to customize
the prompt to their needs or add new partials. This can be done by
modifying the `CreatePlanPrompt.handlebars` file.

New config enables the ability to call the HandlebarsPlanner with:
- a set of KernelArguments, which will serve as the initial context for
the Handlebars Prompt Template; available during plan creation or
execution
- additional prompt context: any domain knowledge or specific content
that might help the model better fulfill the goal when creating a plan
Additional content after the user's goal, with variables shared in the
same message as the goal and additional context as its own separate user
message.

### 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: Tao Chen <[email protected]>
2024-02-13 21:17:30 +00:00

451 lines
21 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Planning.Handlebars;
using Microsoft.SemanticKernel.Plugins.OpenApi;
using Plugins.DictionaryPlugin;
using RepoUtils;
using xRetry;
using Xunit;
using Xunit.Abstractions;
namespace Examples;
// This example shows how to use the Handlebars sequential planner.
public class Example65_HandlebarsPlanner : BaseTest
{
private static int s_sampleIndex;
private const string CourseraPluginName = "CourseraPlugin";
private void WriteSampleHeading(string name)
{
WriteLine($"======== [Handlebars Planner] Sample {s_sampleIndex++} - Create and Execute Plan with: {name} ========");
}
private async Task<Kernel?> SetupKernelAsync(params string[] pluginDirectoryNames)
{
string apiKey = TestConfiguration.AzureOpenAI.ApiKey;
string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName;
string chatModelId = TestConfiguration.AzureOpenAI.ChatModelId;
string endpoint = TestConfiguration.AzureOpenAI.Endpoint;
if (apiKey == null || chatDeploymentName == null || chatModelId == null || endpoint == null)
{
WriteLine("Azure endpoint, apiKey, deploymentName, or modelId not found. Skipping example.");
return null;
}
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: chatDeploymentName,
endpoint: endpoint,
serviceId: "AzureOpenAIChat",
apiKey: apiKey,
modelId: chatModelId)
.Build();
if (pluginDirectoryNames.Length > 0)
{
if (pluginDirectoryNames[0] == StringParamsDictionaryPlugin.PluginName)
{
kernel.ImportPluginFromType<StringParamsDictionaryPlugin>(StringParamsDictionaryPlugin.PluginName);
}
else if (pluginDirectoryNames[0] == ComplexParamsDictionaryPlugin.PluginName)
{
kernel.ImportPluginFromType<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
{
string folder = RepoFiles.SamplePluginsPath();
foreach (var pluginDirectoryName in pluginDirectoryNames)
{
kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, pluginDirectoryName));
}
}
}
return kernel;
}
private void PrintPlannerDetails(string goal, HandlebarsPlan plan, string result, bool shouldPrintPrompt = false)
{
WriteLine($"Goal: {goal}");
WriteLine($"\nOriginal plan:\n{plan}");
WriteLine($"\nResult:\n{result}\n");
// Print the prompt template
if (shouldPrintPrompt && plan.Prompt is not null)
{
WriteLine("\n======== Prompt Template ========");
WriteLine(plan.Prompt);
}
}
private async Task RunSampleAsync(string goal, KernelArguments? initialContext = null, bool shouldPrintPrompt = false, params string[] pluginDirectoryNames)
{
var kernel = await SetupKernelAsync(pluginDirectoryNames);
if (kernel is null)
{
return;
}
// 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 = TestConfiguration.AzureOpenAI.ChatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase);
var planner = new HandlebarsPlanner(
new HandlebarsPlannerOptions()
{
// When using OpenAI models, we recommend using low values for temperature and top_p to minimize planner hallucinations.
ExecutionSettings = new OpenAIPromptExecutionSettings()
{
Temperature = 0.0,
TopP = 0.1,
},
// Change this if you want to test with loops regardless of model selection.
AllowLoops = allowLoopsInPlan
});
// Create the plan
var plan = await planner.CreatePlanAsync(kernel, goal, initialContext);
// Execute the plan
var result = await plan.InvokeAsync(kernel, initialContext);
PrintPlannerDetails(goal, plan, result, shouldPrintPrompt);
}
[RetryTheory(typeof(HttpOperationException))]
[InlineData(false)]
public async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeading("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.", null, shouldPrintPrompt, "SummarizePlugin");
}
catch (KernelException ex) when (
ex.Message.Contains(nameof(HandlebarsPlannerErrorCodes.InsufficientFunctionsForGoal), StringComparison.CurrentCultureIgnoreCase)
|| ex.Message.Contains(nameof(HandlebarsPlannerErrorCodes.HallucinatedHelpers), StringComparison.CurrentCultureIgnoreCase)
|| ex.Message.Contains(nameof(HandlebarsPlannerErrorCodes.InvalidTemplate), StringComparison.CurrentCultureIgnoreCase))
{
/*
[InsufficientFunctionsForGoal] Unable to create plan for goal with available functions.
Goal: Send Mary an email with the list of meetings I have scheduled today.
Available Functions: SummarizePlugin-MakeAbstractReadable, SummarizePlugin-Notegen, SummarizePlugin-Summarize, SummarizePlugin-Topics
Planner output:
As the available helpers do not contain any functionality to send an email or interact with meeting scheduling data, I cannot create a template to achieve the stated goal.
Additional helpers or information may be required.
*/
WriteLine($"\n{ex.Message}\n");
}
}
[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]
public Task RunCourseraSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeading("Coursera OpenAPI Plugin");
return RunSampleAsync("Show me courses about Artificial Intelligence.", null, shouldPrintPrompt, CourseraPluginName);
/*
Original plan:
{{!-- Step 0: Extract key values --}}
{{set "query" "Artificial Intelligence"}}
{{!-- Step 1: Call CourseraPlugin-search with the query --}}
{{set "searchResults" (CourseraPlugin-search query=query)}}
{{!-- Step 2: Loop through the search results and display course information --}}
{{#each searchResults.hits}}
{{json (concat "Course Name: " this.name ", URL: " this.objectUrl)}}
{{/each}}
Result:
Course Name: Introduction to Artificial Intelligence (AI), URL: https://www.coursera.org/learn/introduction-to-ai?utm_source=rest_api
Course Name: IBM Applied AI, URL: https://www.coursera.org/professional-certificates/applied-artifical-intelligence-ibm-watson-ai?utm_source=rest_api
Course Name: AI For Everyone, URL: https://www.coursera.org/learn/ai-for-everyone?utm_source=rest_api
Course Name: Python for Data Science, AI & Development, URL: https://www.coursera.org/learn/python-for-applied-data-science-ai?utm_source=rest_api
Course Name: Introduction to Generative AI, URL: https://www.coursera.org/learn/introduction-to-generative-ai?utm_source=rest_api
Course Name: Deep Learning, URL: https://www.coursera.org/specializations/deep-learning?utm_source=rest_api
Course Name: Machine Learning, URL: https://www.coursera.org/specializations/machine-learning-introduction?utm_source=rest_api
Course Name: IBM AI Engineering, URL: https://www.coursera.org/professional-certificates/ai-engineer?utm_source=rest_api
*/
}
[RetryTheory(typeof(HttpOperationException))]
[InlineData(false)]
public Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeading("Basic Types using Local Dictionary Plugin");
return RunSampleAsync("Get a random word and its definition.", null, shouldPrintPrompt, StringParamsDictionaryPlugin.PluginName);
/*
Original plan:
{{!-- Step 1: Get a random word --}}
{{set "randomWord" (DictionaryPlugin-GetRandomWord)}}
{{!-- Step 2: Get the definition of the random word --}}
{{set "definition" (DictionaryPlugin-GetDefinition word=(get "randomWord"))}}
{{!-- Step 3: Output the random word and its definition --}}
{{json (array (get "randomWord") (get "definition"))}}
Result:
["book","a set of printed or written pages bound together along one edge"]
*/
}
[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]
public Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeading("Complex Types using Local Dictionary Plugin");
return RunSampleAsync("Teach me two random words and their definition.", null, 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
*/
}
[RetryTheory(typeof(HttpOperationException))]
[InlineData(false)]
public Task RunPoetrySampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeading("Multiple Plugins");
return RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", null, shouldPrintPrompt, "SummarizePlugin", "WriterPlugin");
/*
Original plan:
{{!-- Step 1: Initialize the scenario for the poem --}}
{{set "scenario" "John Doe, a mysterious and kind-hearted person"}}
{{!-- Step 2: Generate a short poem about John Doe --}}
{{set "poem" (WriterPlugin-ShortPoem input=(get "scenario"))}}
{{!-- Step 3: Translate the poem into Italian --}}
{{set "translatedPoem" (WriterPlugin-Translate input=(get "poem") language="Italian")}}
{{!-- Step 4: Output the translated poem --}}
{{json (get "translatedPoem")}}
Result:
C'era una volta un uomo di nome John Doe,
La cui gentilezza si mostrava costantemente,
Aiutava con un sorriso,
E non si arrendeva mai,
Al mistero che lo faceva brillare.
*/
}
[RetryTheory(typeof(HttpOperationException))]
[InlineData(false)]
public Task RunBookSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeading("Loops and Conditionals");
return RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", null, shouldPrintPrompt, "WriterPlugin", "MiscPlugin");
/*
Original plan:
{{!-- Step 1: Initialize the book title and chapter count --}}
{{set "bookTitle" "The Thinking Caps"}}
{{set "chapterCount" 3}}
{{!-- Step 2: Generate the novel outline with the given chapter count --}}
{{set "novelOutline" (WriterPlugin-NovelOutline input=(get "bookTitle") chapterCount=(get "chapterCount"))}}
{{!-- Step 3: Loop through the chapters and generate the content for each chapter --}}
{{#each (range 1 (get "chapterCount"))}}
{{set "chapterIndex" this}}
{{set "chapterSynopsis" (MiscPlugin-ElementAtIndex input=(get "novelOutline") index=(get "chapterIndex"))}}
{{set "previousChapterSynopsis" (MiscPlugin-ElementAtIndex input=(get "novelOutline") index=(get "chapterIndex" - 1))}}
{{!-- Step 4: Write the chapter content using the WriterPlugin-NovelChapter helper --}}
{{set "chapterContent" (WriterPlugin-NovelChapter input=(get "chapterSynopsis") theme=(get "bookTitle") previousChapter=(get "previousChapterSynopsis") chapterIndex=(get "chapterIndex"))}}
{{!-- Step 5: Output the chapter content --}}
{{json (get "chapterContent")}}
{{/each}}
*/
}
[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]
public Task RunPredefinedVariablesSample(bool shouldPrintPrompt = false)
{
WriteSampleHeading("CreatePlan Prompt With Predefined Variables");
// When using predefined variables, you must pass these arguments to both the CreatePlanAsync and InvokeAsync methods.
var initialArguments = new KernelArguments()
{
{ "greetings", new List<string>(){ "hey", "bye" } },
{ "someNumber", 1 },
{ "person", new Dictionary<string, string>()
{
{"name", "John Doe" },
{ "language", "Italian" },
} }
};
return RunSampleAsync("Write a poem about the given person, then translate it into French.", initialArguments, shouldPrintPrompt, "WriterPlugin", "MiscPlugin");
/*
Original plan:
{{!-- Step 0: Set the given person --}}
{{set "person" "John Doe"}}
{{!-- Step 1: Generate a short poem about the person --}}
{{set "poem" (WriterPlugin-ShortPoem input=person)}}
{{!-- Step 2: Translate the poem into French --}}
{{set "translatedPoem" (WriterPlugin-Translate input=poem language="French")}}
{{!-- Step 3: Output the translated poem --}}
{{json translatedPoem}}
Result:
Il était une fois un gars nommé Doe,
Dont la vie était un spectacle comique,
Il trébuchait et tombait,
Mais riait à travers tout cela,
Alors qu'il dansait dans la vie, de-ci de-là.
*/
}
[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]
public async Task RunPromptWithAdditionalContextSampleAsync(bool shouldPrintPrompt = false)
{
WriteSampleHeading("Prompt With Additional Context");
// Pulling the raw content from SK's README file as domain context.
static async Task<string> getDomainContext()
{
// For demonstration purposes only, beware of token count.
var repositoryUrl = "https://github.com/microsoft/semantic-kernel";
var readmeUrl = $"{repositoryUrl}/main/README.md".Replace("github.com", "raw.githubusercontent.com", StringComparison.CurrentCultureIgnoreCase);
try
{
var httpClient = new HttpClient();
// Send a GET request to the specified URL
var response = await httpClient.GetAsync(new Uri(readmeUrl));
response.EnsureSuccessStatusCode(); // Throw an exception if not successful
// Read the response content as a string
var content = await response.Content.ReadAsStringAsync();
httpClient.Dispose();
return "Content imported from the README of https://github.com/microsoft/semantic-kernel:\n" + content;
}
catch (HttpRequestException e)
{
Console.WriteLine("\nException Caught!");
Console.WriteLine("Message :{0} ", e.Message);
return "";
}
}
var goal = "Help me onboard to the Semantic Kernel SDK by creating a quick guide that includes a brief overview of the SDK for C# developers and detailed set-up steps. Include relevant links where possible. Then, draft an email with this guide, so I can share it with my team.";
var kernel = await SetupKernelAsync("WriterPlugin");
if (kernel is null)
{
return;
}
// 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 = TestConfiguration.AzureOpenAI.ChatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase);
var planner = new HandlebarsPlanner(
new HandlebarsPlannerOptions()
{
// Context to be used in the prompt template.
GetAdditionalPromptContext = getDomainContext,
AllowLoops = false
});
// Create the plan
var plan = await planner.CreatePlanAsync(kernel, goal);
// Execute the plan
var result = await plan.InvokeAsync(kernel);
PrintPlannerDetails(goal, plan, result, shouldPrintPrompt);
/*
{{!-- Step 0: Extract Key Values --}}
{{set "sdkLink" "https://learn.microsoft.com/en-us/semantic-kernel/overview/"}}
{{set "nugetPackageLink" "https://www.nuget.org/packages/Microsoft.SemanticKernel/"}}
{{set "csharpGetStartedLink" "dotnet/README.md"}}
{{set "emailSubject" "Semantic Kernel SDK: Quick Guide for C# Developers"}}
{{!-- Step 1: Create a concise guide and store it in a variable --}}
{{set "guide" (concat "The Semantic Kernel SDK provides seamless integration between large language models (LLMs) and programming languages such as C#. " "To get started with the C# SDK, please follow these steps:\n\n" "1. Read the SDK Overview for a brief introduction here: " sdkLink "\n" "2. Install the Nuget package in your project: " nugetPackageLink "\n" "3. Follow the detailed set-up steps in the C# 'Getting Started' guide: " csharpGetStartedLink "\n\n" "Feel free to share this quick guide with your team members to help them onboard quickly with the Semantic Kernel SDK. ")}}
{{!-- Step 2: Generate a draft email with the guide --}}
{{set "emailBody" (concat "Hi Team,\n\n" "I have put together a quick guide to help you onboard to the Semantic Kernel SDK for C# developers. " "This guide includes a brief overview and detailed set-up steps:\n\n" guide "\n\n" "I have attached a more comprehensive guide as a document. Please review it and let me know if you have any questions. " "Let's start integrating the Semantic Kernel SDK into our projects!\n\n" "Best Regards,\n" "Your Name ")}}
{{json (concat "Subject: " emailSubject "\n\nBody:\n" emailBody)}}
Result:
Subject: Semantic Kernel SDK: Quick Guide for C# Developers
Body:
Hi Team,
I have put together a quick guide to help you onboard to the Semantic Kernel SDK for C# developers. This guide includes a brief overview and detailed set-up steps:
The Semantic Kernel SDK provides seamless integration between large language models (LLMs) and programming languages such as C#. To get started with the C# SDK, please follow these steps:
1. Read the SDK Overview for a brief introduction here: https://learn.microsoft.com/en-us/semantic-kernel/overview/
2. Install the Nuget package in your project: https://www.nuget.org/packages/Microsoft.SemanticKernel/
3. Follow the detailed set-up steps in the C# 'Getting Started' guide: dotnet/README.md
Feel free to share this quick guide with your team members to help them onboard quickly with the Semantic Kernel SDK.
I have attached a more comprehensive guide as a document. Please review it and let me know if you have any questions. Let's start integrating the Semantic Kernel SDK into our projects!
Best Regards,
Your Name
*/
}
public Example65_HandlebarsPlanner(ITestOutputHelper output) : base(output)
{
}
}