Skip to content

Commit

Permalink
.Net: Create Plan Prompt Override (#5005)
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.
-->

This PR adds the option to override the CreatePlan prompt.

### Description

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

Users can pass in a callback to
`HandlebarsPlannerOptions.GetCreatePlanPrompt` that returns a valid
Handlebars Template string to be used in place of the default CreatePlan
prompt.

Users have the options to select any predefined partials in their own
template. These helpers are pre-registered and available for reference
at the time of import override.

### 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: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
  • Loading branch information
teresaqhoang and markwallace-microsoft committed Feb 15, 2024
1 parent 0a60d9c commit 27d5ffe
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 68 deletions.
128 changes: 71 additions & 57 deletions dotnet/samples/KernelSyntaxExamples/Example65_HandlebarsPlanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.SemanticKernel.Plugins.OpenApi;
using Plugins.DictionaryPlugin;
using RepoUtils;
using Resources;
using xRetry;
using Xunit;
using Xunit.Abstractions;
Expand Down Expand Up @@ -82,7 +83,7 @@ private void WriteSampleHeading(string name)
return kernel;
}

private void PrintPlannerDetails(string goal, HandlebarsPlan plan, string result, bool shouldPrintPrompt = false)
private void PrintPlannerDetails(string goal, HandlebarsPlan plan, string result, bool shouldPrintPrompt)
{
WriteLine($"Goal: {goal}");
WriteLine($"\nOriginal plan:\n{plan}");
Expand All @@ -91,55 +92,60 @@ private void PrintPlannerDetails(string goal, HandlebarsPlan plan, string result
// Print the prompt template
if (shouldPrintPrompt && plan.Prompt is not null)
{
WriteLine("\n======== Prompt Template ========");
WriteLine("\n======== CreatePlan Prompt ========");
WriteLine(plan.Prompt);
}
}

private async Task RunSampleAsync(string goal, KernelArguments? initialContext = null, bool shouldPrintPrompt = false, params string[] pluginDirectoryNames)
private async Task RunSampleAsync(
string goal,
HandlebarsPlannerOptions? plannerOptions = null,
KernelArguments? initialContext = null,
bool shouldPrintPrompt = false,
bool shouldInvokePlan = true,
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()
// Set the planner options
plannerOptions ??= new HandlebarsPlannerOptions()
{
// When using OpenAI models, we recommend using low values for temperature and top_p to minimize planner hallucinations.
ExecutionSettings = new OpenAIPromptExecutionSettings()
{
// 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,
},
Temperature = 0.0,
TopP = 0.1,
},
};

// Change this if you want to test with loops regardless of model selection.
AllowLoops = allowLoopsInPlan
});
// 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.
plannerOptions.AllowLoops = TestConfiguration.AzureOpenAI.ChatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase);

// Create the plan
// Instantiate the planner and create the plan
var planner = new HandlebarsPlanner(plannerOptions);
var plan = await planner.CreatePlanAsync(kernel, goal, initialContext);

// Execute the plan
var result = await plan.InvokeAsync(kernel, initialContext);
var result = shouldInvokePlan ? await plan.InvokeAsync(kernel, initialContext) : string.Empty;

PrintPlannerDetails(goal, plan, result, shouldPrintPrompt);
}

[RetryTheory(typeof(HttpOperationException))]
[InlineData(false)]
public async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt = false)
public async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt)
{
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");
await RunSampleAsync("Send Mary an email with the list of meetings I have scheduled today.", null, null, shouldPrintPrompt, true, "SummarizePlugin");
}
catch (KernelException ex) when (
ex.Message.Contains(nameof(HandlebarsPlannerErrorCodes.InsufficientFunctionsForGoal), StringComparison.CurrentCultureIgnoreCase)
Expand All @@ -161,10 +167,10 @@ public async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt = false)
[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]

public Task RunCourseraSampleAsync(bool shouldPrintPrompt = false)
public Task RunCourseraSampleAsync(bool shouldPrintPrompt)
{
WriteSampleHeading("Coursera OpenAPI Plugin");
return RunSampleAsync("Show me courses about Artificial Intelligence.", null, shouldPrintPrompt, CourseraPluginName);
return RunSampleAsync("Show me courses about Artificial Intelligence.", null, null, shouldPrintPrompt, true, CourseraPluginName);
/*
Original plan:
{{!-- Step 0: Extract key values --}}
Expand Down Expand Up @@ -193,10 +199,10 @@ public Task RunCourseraSampleAsync(bool shouldPrintPrompt = false)

[RetryTheory(typeof(HttpOperationException))]
[InlineData(false)]
public Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt = false)
public Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt)
{
WriteSampleHeading("Basic Types using Local Dictionary Plugin");
return RunSampleAsync("Get a random word and its definition.", null, shouldPrintPrompt, StringParamsDictionaryPlugin.PluginName);
return RunSampleAsync("Get a random word and its definition.", null, null, shouldPrintPrompt, true, StringParamsDictionaryPlugin.PluginName);
/*
Original plan:
{{!-- Step 1: Get a random word --}}
Expand All @@ -215,10 +221,10 @@ public Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt = fals

[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]
public Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt = false)
public Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt)
{
WriteSampleHeading("Complex Types using Local Dictionary Plugin");
return RunSampleAsync("Teach me two random words and their definition.", null, shouldPrintPrompt, ComplexParamsDictionaryPlugin.PluginName);
return RunSampleAsync("Teach me two random words and their definition.", null, null, shouldPrintPrompt, true, ComplexParamsDictionaryPlugin.PluginName);
/*
Original Plan:
{{!-- Step 1: Get two random dictionary entries --}}
Expand Down Expand Up @@ -251,10 +257,10 @@ public Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt

[RetryTheory(typeof(HttpOperationException))]
[InlineData(false)]
public Task RunPoetrySampleAsync(bool shouldPrintPrompt = false)
public Task RunPoetrySampleAsync(bool shouldPrintPrompt)
{
WriteSampleHeading("Multiple Plugins");
return RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", null, shouldPrintPrompt, "SummarizePlugin", "WriterPlugin");
return RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", null, null, shouldPrintPrompt, true, "SummarizePlugin", "WriterPlugin");
/*
Original plan:
{{!-- Step 1: Initialize the scenario for the poem --}}
Expand All @@ -280,10 +286,10 @@ public Task RunPoetrySampleAsync(bool shouldPrintPrompt = false)

[RetryTheory(typeof(HttpOperationException))]
[InlineData(false)]
public Task RunBookSampleAsync(bool shouldPrintPrompt = false)
public Task RunBookSampleAsync(bool shouldPrintPrompt)
{
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");
return RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", null, null, shouldPrintPrompt, true, "WriterPlugin", "MiscPlugin");
/*
Original plan:
{{!-- Step 1: Initialize the book title and chapter count --}}
Expand All @@ -310,7 +316,7 @@ public Task RunBookSampleAsync(bool shouldPrintPrompt = false)

[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]
public Task RunPredefinedVariablesSample(bool shouldPrintPrompt = false)
public Task RunPredefinedVariablesSampleAsync(bool shouldPrintPrompt)
{
WriteSampleHeading("CreatePlan Prompt With Predefined Variables");

Expand All @@ -326,7 +332,7 @@ public Task RunPredefinedVariablesSample(bool shouldPrintPrompt = false)
} }
};

return RunSampleAsync("Write a poem about the given person, then translate it into French.", initialArguments, shouldPrintPrompt, "WriterPlugin", "MiscPlugin");
return RunSampleAsync("Write a poem about the given person, then translate it into French.", null, initialArguments, shouldPrintPrompt, true, "WriterPlugin", "MiscPlugin");
/*
Original plan:
{{!-- Step 0: Set the given person --}}
Expand All @@ -352,7 +358,7 @@ public Task RunPredefinedVariablesSample(bool shouldPrintPrompt = false)

[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]
public async Task RunPromptWithAdditionalContextSampleAsync(bool shouldPrintPrompt = false)
public Task RunPromptWithAdditionalContextSampleAsync(bool shouldPrintPrompt)
{
WriteSampleHeading("Prompt With Additional Context");

Expand Down Expand Up @@ -383,31 +389,13 @@ static async Task<string> getDomainContext()
}

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)
var plannerOptions = new HandlebarsPlannerOptions()
{
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);
// Context to be used in the prompt template.
GetAdditionalPromptContext = getDomainContext,
};

PrintPlannerDetails(goal, plan, result, shouldPrintPrompt);
return RunSampleAsync(goal, plannerOptions, null, shouldPrintPrompt, true, "WriterPlugin");
/*
{{!-- Step 0: Extract Key Values --}}
{{set "sdkLink" "https://learn.microsoft.com/en-us/semantic-kernel/overview/"}}
Expand Down Expand Up @@ -444,6 +432,32 @@ Your Name
*/
}

[RetryTheory(typeof(HttpOperationException))]
[InlineData(true)]
public Task RunOverrideCreatePlanPromptSampleAsync(bool shouldPrintPrompt)
{
WriteSampleHeading("CreatePlan Prompt Override");

static string OverridePlanPrompt()
{
// Load a custom CreatePlan prompt template from an embedded resource.
var ResourceFileName = "65-prompt-override.handlebars";
var fileContent = EmbeddedResource.ReadStream(ResourceFileName);
return new StreamReader(fileContent!).ReadToEnd();
}

var plannerOptions = new HandlebarsPlannerOptions()
{
// Callback to override the default prompt template.
CreatePlanPromptHandler = OverridePlanPrompt,
};

var goal = "I just watched the movie 'Inception' and I loved it! I want to leave a 5 star review. Can you help me?";
return RunSampleAsync(goal, plannerOptions, null, shouldPrintPrompt, false, "WriterPlugin");

// For a simpler example, see `ItOverridesPromptAsync` in the dotnet\src\Planners\Planners.Handlebars.UnitTests\Handlebars\HandlebarsPlannerTests.cs file.
}

public Example65_HandlebarsPlanner(ITestOutputHelper output) : base(output)
{
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{{!-- Example of a custom CreatePlan prompt for the Handlebars Planner.
Be sure to use the ChatHistory syntax so the completion request is formatted correctly before it's sent to the model. --}}
{{#message role="system"}}
Explain how to achieve the user's goal using the available helpers with a Handlebars .Net template.
{{~/message}}

{{#message role="user"}}
{{!-- Custom helpers section. Strictly for demonstration purposes. This will only render what's shown below.
It will not render any of the kernel functions or built-in system helpers,
thus overriding the helpers section is not recommended. --}}
You have a plugin named `MovieDatabase`.
These are the object types that are used in the plugin:

### Movie:
{
"type": "Object",
"properties": {
"name": {
"type": "string",
},
"genre": {
"type": "string",
},
"year": {
"type": "integer",
},
"rating": {
"type": "number",
},
}
}

### MovieDetails:
{
"type": "Object",
"properties": {
"name": {
"type": "string",
},
"genre": {
"type": "string",
},
"year": {
"type": "integer",
},
"rating": {
"type": "number",
},
"description": {
"type": "string",
},
"reviews": {
"type": "array",
"items": {
"type": "string",
}
},
"actors": {
"type": "array",
"items": {
"type": "string",
}
},
"director": {
"type": "string",
},
}
}

These are the custom helpers that are available in the `MovieDatabase` plugin:

### `MovieDatabase{{../nameDelimiter}}SearchMovies`
Description: Search for movies in the database.
Inputs:
- Name: string - The name of the movie to search for, can be a partial name. (optional)
- Genre: string - Genre of movie to search for. (optional)
Output: List<Movie> - List of movies matching the search criteria.

### `MovieDatabase{{../nameDelimiter}}GetMovieSummary`
Description: Get of a movie.
Inputs:
- Name: string - The name of the movie to get. (required)
Output: Movie - Summary details of the movie.

### `MovieDatabase{{../nameDelimiter}}GetFullMovieDetails`
Description: Get full details of a movie.
Inputs:
- Movie: Movie - The movie to pull details for. (required)
Output: MovieDetails - Full details of the movie.

### `MovieDatabase{{../nameDelimiter}}AddMovie`
Description: Add a movie to the database.
Inputs:
- Movie: MovieDetails - The movie to add to the database. (required)
Output: Void

### `MovieDatabase{{../nameDelimiter}}AddReview`
Description: Add a review to a movie.
Inputs:
- Movie: Movie - The movie to add the rating to. (required)
- Rating: number - The rating to add to the movie. (required)
- Review: string - The review to add to the movie. (optional)
Output: Void

{{!-- All partials defined in Planners.Handlebars.CreatePlanPromptPartials nanespace are registered
with every Handlebars Planner instance and can be selected for use in custom prompts. --}}
You also have the following built-in helpers available for use:
{{> BlockHelpers }}

{{> VariableHelpers }}
{{/message}}

{{!-- You can inject the user goal manually using {{goal}} or use the UserGoal partial to leverage SK's templating and prompt engineering. --}}
{{> UserGoal }}

{{> TipsAndInstructions }}

0 comments on commit 27d5ffe

Please sign in to comment.