Skip to content

Commit

Permalink
Adding HandlebarsPlanCreationException (#4973)
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.
-->

Resolves #4440

This PR adds a more detailed `HandlebarsPlanCreationException` type,
which allows users to inspect the prompt and model results, if
available.

Als includes a minor update to sample to add clarifying comments.

### Description

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

Users can catch `HandlebarsPlanCreationException` to inspect the prompt,
proposed plan, and exception details on error. All exceptions will be
bubbled up to the caller.

```
var planner = new HandlebarsPlanner();

try
{
     var plan = await planner.CreatePlanAsync(kernel, intent, cancellationToken);
}
catch (HandlebarsPlanCreationException ex)
{
     Console.WriteLine(ex.Message);
     Console.WriteLine(ex.InnerException?.Message);
     Console.WriteLine($"CreatePlan Prompt: {ex.CreatePlanPrompt}");
     Console.WriteLine($"Proposed plan (model output): {ex.ModelResults.Content}");
     throw ex;
}
```

### 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 committed Feb 23, 2024
1 parent d38b45d commit 8858733
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -453,9 +453,11 @@ static string 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");

// Note that since the custom prompt inputs a unique Helpers section with helpers not actually registered with the kernel,
// any plan created using this prompt will fail execution; thus, we will skip the InvokePlan call in this example.
// For a simpler example, see `ItOverridesPromptAsync` in the dotnet\src\Planners\Planners.Handlebars.UnitTests\Handlebars\HandlebarsPlannerTests.cs file.
return RunSampleAsync(goal, plannerOptions, null, shouldPrintPrompt, shouldInvokePlan: false, "WriterPlugin");
}

public Example65_HandlebarsPlanner(ITestOutputHelper output) : base(output)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.

using System;

namespace Microsoft.SemanticKernel.Planning;

/// <summary>
/// Exception thrown when a plan cannot be created.
/// </summary>
public sealed class PlanCreationException : KernelException
{
/// <summary>
/// Gets the prompt template used to generate the plan.
/// </summary>
public string? CreatePlanPrompt { get; set; } = null;

/// <summary>
/// Completion results from the model; generally, this is the proposed plan.
/// </summary>
public ChatMessageContent? ModelResults { get; set; } = null;

/// <summary>
/// Initializes a new instance of the <see cref="PlanCreationException"/> class.
/// </summary>
public PlanCreationException()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="PlanCreationException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
public PlanCreationException(string? message) : base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="PlanCreationException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public PlanCreationException(string? message, Exception? innerException) : base(message, innerException)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="PlanCreationException"/> class.
/// Exception thrown when a plan cannot be created containing the prompt and model results.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="createPlanPrompt">The prompt template used to generate the plan.</param>
/// <param name="modelResults">Completion results from the model; generally, this is the proposed plan.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public PlanCreationException(string? message, string? createPlanPrompt, ChatMessageContent? modelResults, Exception? innerException = null) : base(message, innerException)
{
this.CreatePlanPrompt = createPlanPrompt;
this.ModelResults = modelResults;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.Planning.Handlebars;
using Microsoft.SemanticKernel.Text;
using Moq;
Expand Down Expand Up @@ -61,13 +62,18 @@ public async Task EmptyGoalThrowsAsync()
public async Task InvalidHandlebarsTemplateThrowsAsync()
{
// Arrange
var kernel = this.CreateKernelWithMockCompletionResult("<plan>notvalid<</plan>");
var invalidPlan = "<plan>notvalid<</plan>";
var kernel = this.CreateKernelWithMockCompletionResult(invalidPlan);

var planner = new HandlebarsPlanner();

// Act & Assert
var exception = await Assert.ThrowsAsync<KernelException>(async () => await planner.CreatePlanAsync(kernel, "goal"));
Assert.True(exception?.Message?.Contains("Could not find the plan in the results", StringComparison.InvariantCulture));
var exception = await Assert.ThrowsAsync<PlanCreationException>(async () => await planner.CreatePlanAsync(kernel, "goal"));

Assert.True(exception?.Message?.Contains("CreatePlan failed. See inner exception for details.", StringComparison.InvariantCulture));
Assert.True(exception?.InnerException?.Message?.Contains("Could not find the plan in the results", StringComparison.InvariantCulture));
Assert.Equal(exception?.ModelResults?.Content, invalidPlan);
Assert.NotNull(exception?.CreatePlanPrompt);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,41 @@ public Task<HandlebarsPlan> CreatePlanAsync(Kernel kernel, string goal, KernelAr

private async Task<HandlebarsPlan> CreatePlanCoreAsync(Kernel kernel, string goal, KernelArguments? arguments, CancellationToken cancellationToken = default)
{
// Get CreatePlan prompt template
var functionsMetadata = await kernel.Plugins.GetFunctionsAsync(this._options, null, null, cancellationToken).ConfigureAwait(false);
var availableFunctions = this.GetAvailableFunctionsManual(functionsMetadata, out var complexParameterTypes, out var complexParameterSchemas);
var createPlanPrompt = await this.GetHandlebarsTemplateAsync(kernel, goal, arguments, availableFunctions, complexParameterTypes, complexParameterSchemas, cancellationToken).ConfigureAwait(false);
ChatHistory chatMessages = this.GetChatHistoryFromPrompt(createPlanPrompt);

// Get the chat completion results
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var completionResults = await chatCompletionService.GetChatMessageContentAsync(chatMessages, executionSettings: this._options.ExecutionSettings, cancellationToken: cancellationToken).ConfigureAwait(false);

Match match = Regex.Match(completionResults.Content, @"```\s*(handlebars)?\s*(.*)\s*```", RegexOptions.Singleline);
if (!match.Success)
string? createPlanPrompt = null;
ChatMessageContent? modelResults = null;

try
{
throw new KernelException($"[{HandlebarsPlannerErrorCodes.InvalidTemplate}] Could not find the plan in the results. Additional helpers or input may be required.\n\nPlanner output:\n{completionResults}");
}
// Get CreatePlan prompt template
var functionsMetadata = await kernel.Plugins.GetFunctionsAsync(this._options, null, null, cancellationToken).ConfigureAwait(false);
var availableFunctions = this.GetAvailableFunctionsManual(functionsMetadata, out var complexParameterTypes, out var complexParameterSchemas);
createPlanPrompt = await this.GetHandlebarsTemplateAsync(kernel, goal, arguments, availableFunctions, complexParameterTypes, complexParameterSchemas, cancellationToken).ConfigureAwait(false);
ChatHistory chatMessages = this.GetChatHistoryFromPrompt(createPlanPrompt);

// Get the chat completion results
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
modelResults = await chatCompletionService.GetChatMessageContentAsync(chatMessages, executionSettings: this._options.ExecutionSettings, cancellationToken: cancellationToken).ConfigureAwait(false);

Match match = Regex.Match(modelResults.Content, @"```\s*(handlebars)?\s*(.*)\s*```", RegexOptions.Singleline);
if (!match.Success)
{
throw new KernelException($"[{HandlebarsPlannerErrorCodes.InvalidTemplate}] Could not find the plan in the results.");
}

var planTemplate = match.Groups[2].Value.Trim();
planTemplate = MinifyHandlebarsTemplate(planTemplate);
var planTemplate = match.Groups[2].Value.Trim();
planTemplate = MinifyHandlebarsTemplate(planTemplate);

return new HandlebarsPlan(planTemplate, createPlanPrompt);
return new HandlebarsPlan(planTemplate, createPlanPrompt);
}
catch (KernelException ex)
{
throw new PlanCreationException(
"CreatePlan failed. See inner exception for details.",
createPlanPrompt,
modelResults,
ex
);
}
}

private List<KernelFunctionMetadata> GetAvailableFunctionsManual(
Expand Down

0 comments on commit 8858733

Please sign in to comment.