Skip to content

Commit

Permalink
CopilotChat: Migrate to ActionPlanner (microsoft#765)
Browse files Browse the repository at this point in the history
### Motivation and Context
The SequentialPlanner needs GPT-4 to operate with relative consistency
and correctness. Using ActionPlanner simplifies the planning feature set
but also allows us to use gpt-3.5-turbo with single step plans.

### Description
- Updated CopilotChat planner feature flag to enabled=true
- Updated CopilotChatPlanner to use ActionPlanner (single step planner)
- Added an AIService configuration for CopilotChat's planner to
disaggregate the completion model with the planner model.
- CopilotChat won't invoke plans with no steps.
- Removed core and semantic skills from CopilotChat planner manuals
- ActionPlanner: Added support for non-string parameters
- ActionPlanner: Added JSON property sanitation to 'rationale' values
(replace double quotes with single and remove newlines)
- ActionPlanner: fixed JSON bug when no relevant function is found
(extra close curly brace)
  • Loading branch information
adrianwyatt committed May 2, 2023
1 parent ebf7556 commit 80a43ef
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 149 deletions.
74 changes: 5 additions & 69 deletions samples/apps/copilot-chat-app/webapi/Config/PlannerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

using System.ComponentModel.DataAnnotations;
using Microsoft.SemanticKernel.Planning.Sequential;

namespace SemanticKernel.Service.Config;

Expand All @@ -13,76 +12,13 @@ public class PlannerOptions
public const string PropertyName = "Planner";

/// <summary>
/// Whether to enable the planner.
/// </summary>
public bool Enabled { get; set; } = false;

/// <summary>
/// The directory containing semantic skills to include in the planner's list of available functions.
/// </summary>
public string? SemanticSkillsDirectory { get; set; }

/// <summary>
/// The minimum relevancy score for a function to be considered
/// The AI service to use for planning.
/// </summary>
public double? RelevancyThreshold { get; set; }
[Required]
public AIServiceOptions? AIService { get; set; }

/// <summary>
/// The maximum number of relevant functions to include in the plan.
/// </summary>
public int MaxRelevantFunctions { get; set; } = 100;

/// <summary>
/// A list of skills to exclude from the plan creation request.
/// </summary>
public HashSet<string> ExcludedSkills { get; set; } = new();

/// <summary>
/// A list of functions to exclude from the plan creation request.
/// </summary>
public HashSet<string> ExcludedFunctions { get; set; } = new();

/// <summary>
/// A list of functions to include in the plan creation request.
/// </summary>
public HashSet<string> IncludedFunctions { get; set; } = new();

/// <summary>
/// The maximum number of tokens to allow in a plan.
/// </summary>
[Range(1, int.MaxValue)]
public int MaxTokens { get; set; } = 1024;

/// <summary>
/// Convert to a <see cref="SequentialPlannerConfig"/> instance.
/// Whether to enable the planner.
/// </summary>
public SequentialPlannerConfig ToSequentialPlannerConfig()
{
SequentialPlannerConfig config = new()
{
RelevancyThreshold = this.RelevancyThreshold,
MaxRelevantFunctions = this.MaxRelevantFunctions,
MaxTokens = this.MaxTokens,
};

this.ExcludedSkills.Clear();
foreach (var excludedSkill in this.ExcludedSkills)
{
config.ExcludedSkills.Add(excludedSkill);
}

this.ExcludedFunctions.Clear();
foreach (var excludedFunction in this.ExcludedFunctions)
{
config.ExcludedFunctions.Add(excludedFunction);
}

this.IncludedFunctions.Clear();
foreach (var includedFunction in this.IncludedFunctions)
{
config.IncludedFunctions.Add(includedFunction);
}

return config;
}
public bool Enabled { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,27 +136,20 @@ public class SemanticKernelController : ControllerBase
/// </summary>
private async Task RegisterPlannerSkillsAsync(CopilotChatPlanner planner, PlannerOptions options, OpenApiSkillsAuthHeaders openApiSkillsAuthHeaders)
{
await planner.Kernel.ImportChatGptPluginSkillFromUrlAsync("KlarnaShopping", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"));
// Register the Klarna shopping skill with the planner's kernel.
await planner.Kernel.ImportOpenApiSkillFromFileAsync(
skillName: "KlarnaShoppingSkill",
filePath: Path.Combine(Directory.GetCurrentDirectory(), @"Skills/OpenApiSkills/KlarnaSkill/openapi.json"));

// Register authenticated OpenAPI skills with the planner's kernel
// if the request includes an auth header for an OpenAPI skill.
// Else, don't register the skill as it'll fail on auth.
// Register authenticated OpenAPI skills with the planner's kernel if the request includes an auth header for an OpenAPI skill.
if (openApiSkillsAuthHeaders.GithubAuthentication != null)
{
var authenticationProvider = new BearerAuthenticationProvider(() => { return Task.FromResult(openApiSkillsAuthHeaders.GithubAuthentication); });
this._logger.LogInformation("Registering GitHub Skill");

var filePath = Path.Combine(Directory.GetCurrentDirectory(), @"Skills/OpenApiSkills/GitHubSkill/openapi.json");
var skill = await planner.Kernel.ImportOpenApiSkillFromFileAsync("GitHubSkill", filePath, authenticationProvider.AuthenticateRequestAsync);
}

planner.Kernel.ImportSkill(new Microsoft.SemanticKernel.CoreSkills.TextSkill(), "text");
planner.Kernel.ImportSkill(new Microsoft.SemanticKernel.CoreSkills.TimeSkill(), "time");
planner.Kernel.ImportSkill(new Microsoft.SemanticKernel.CoreSkills.MathSkill(), "math");

if (!string.IsNullOrWhiteSpace(options.SemanticSkillsDirectory))
{
planner.Kernel.RegisterSemanticSkills(options.SemanticSkillsDirectory, this._logger);
BearerAuthenticationProvider authenticationProvider = new(() => Task.FromResult(openApiSkillsAuthHeaders.GithubAuthentication));
await planner.Kernel.ImportOpenApiSkillFromFileAsync(
skillName: "GitHubSkill",
filePath: Path.Combine(Directory.GetCurrentDirectory(), @"Skills/OpenApiSkills/GitHubSkill/openapi.json"),
authCallback: authenticationProvider.AuthenticateRequestAsync);
}
}
}
2 changes: 1 addition & 1 deletion samples/apps/copilot-chat-app/webapi/CopilotChatApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<ProjectReference Include="..\..\..\..\dotnet\src\Connectors\Connectors.AI.OpenAI\Connectors.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Skills\Skills.OpenAPI\Skills.OpenAPI.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Connectors\Connectors.Memory.Qdrant\Connectors.Memory.Qdrant.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Extensions\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Extensions\Planning.ActionPlanner\Planning.ActionPlanner.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\SemanticKernel\SemanticKernel.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Skills\Skills.OpenAPI\Skills.OpenAPI.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Skills\Skills.Web\Skills.Web.csproj" />
Expand Down
53 changes: 26 additions & 27 deletions samples/apps/copilot-chat-app/webapi/SemanticKernelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,27 @@ internal static IServiceCollection AddSemanticKernelServices(this IServiceCollec
// Add the planner.
services.AddScoped<CopilotChatPlanner>(sp =>
{
// Create a kernel for the planner with the same contexts as the chat's kernel except with no skills.
// Create a kernel for the planner with the same contexts as the chat's kernel except with no skills and its own completion backend.
// This allows the planner to use only the skills that are available at call time.
IKernel chatKernel = sp.GetRequiredService<IKernel>();
IOptions<PlannerOptions> plannerOptions = sp.GetRequiredService<IOptions<PlannerOptions>>();
IKernel plannerKernel = new Kernel(
new SkillCollection(),
chatKernel.PromptTemplateEngine,
chatKernel.Memory,
chatKernel.Config,
new KernelConfig().AddCompletionBackend(plannerOptions.Value.AIService!),
sp.GetRequiredService<ILogger<CopilotChatPlanner>>());
return new CopilotChatPlanner(plannerKernel, sp.GetRequiredService<IOptions<PlannerOptions>>());
return new CopilotChatPlanner(plannerKernel, plannerOptions);
});

// Add the Semantic Kernel
services.AddSingleton<IPromptTemplateEngine, PromptTemplateEngine>();
services.AddScoped<ISkillCollection, SkillCollection>();
services.AddScoped<KernelConfig>(serviceProvider => new KernelConfig()
.AddCompletionBackend(serviceProvider.GetRequiredService<IOptionsSnapshot<AIServiceOptions>>())
.AddEmbeddingBackend(serviceProvider.GetRequiredService<IOptionsSnapshot<AIServiceOptions>>()));
.AddCompletionBackend(serviceProvider.GetRequiredService<IOptionsSnapshot<AIServiceOptions>>()
.Get(AIServiceOptions.CompletionPropertyName))
.AddEmbeddingBackend(serviceProvider.GetRequiredService<IOptionsSnapshot<AIServiceOptions>>()
.Get(AIServiceOptions.EmbeddingPropertyName)));
services.AddScoped<IKernel, Kernel>();

return services;
Expand All @@ -96,27 +99,25 @@ internal static IServiceCollection AddSemanticKernelServices(this IServiceCollec
/// <summary>
/// Add the completion backend to the kernel config
/// </summary>
internal static KernelConfig AddCompletionBackend(this KernelConfig kernelConfig, IOptionsSnapshot<AIServiceOptions> aiServiceOptions)
internal static KernelConfig AddCompletionBackend(this KernelConfig kernelConfig, AIServiceOptions aiServiceOptions)
{
AIServiceOptions config = aiServiceOptions.Get(AIServiceOptions.CompletionPropertyName);

switch (config.AIService)
switch (aiServiceOptions.AIService)
{
case AIServiceOptions.AIServiceType.AzureOpenAI:
kernelConfig.AddAzureChatCompletionService(
deploymentName: config.DeploymentOrModelId,
endpoint: config.Endpoint,
apiKey: config.Key);
deploymentName: aiServiceOptions.DeploymentOrModelId,
endpoint: aiServiceOptions.Endpoint,
apiKey: aiServiceOptions.Key);
break;

case AIServiceOptions.AIServiceType.OpenAI:
kernelConfig.AddOpenAIChatCompletionService(
modelId: config.DeploymentOrModelId,
apiKey: config.Key);
modelId: aiServiceOptions.DeploymentOrModelId,
apiKey: aiServiceOptions.Key);
break;

default:
throw new ArgumentException($"Invalid {nameof(config.AIService)} value in '{AIServiceOptions.CompletionPropertyName}' settings.");
throw new ArgumentException($"Invalid {nameof(aiServiceOptions.AIService)} value in '{AIServiceOptions.CompletionPropertyName}' settings.");
}

return kernelConfig;
Expand All @@ -125,29 +126,27 @@ internal static KernelConfig AddCompletionBackend(this KernelConfig kernelConfig
/// <summary>
/// Add the embedding backend to the kernel config
/// </summary>
internal static KernelConfig AddEmbeddingBackend(this KernelConfig kernelConfig, IOptionsSnapshot<AIServiceOptions> aiServiceOptions)
internal static KernelConfig AddEmbeddingBackend(this KernelConfig kernelConfig, AIServiceOptions aiServiceOptions)
{
AIServiceOptions config = aiServiceOptions.Get(AIServiceOptions.EmbeddingPropertyName);

switch (config.AIService)
switch (aiServiceOptions.AIService)
{
case AIServiceOptions.AIServiceType.AzureOpenAI:
kernelConfig.AddAzureTextEmbeddingGenerationService(
deploymentName: config.DeploymentOrModelId,
endpoint: config.Endpoint,
apiKey: config.Key,
serviceId: config.Label);
deploymentName: aiServiceOptions.DeploymentOrModelId,
endpoint: aiServiceOptions.Endpoint,
apiKey: aiServiceOptions.Key,
serviceId: aiServiceOptions.Label);
break;

case AIServiceOptions.AIServiceType.OpenAI:
kernelConfig.AddOpenAITextEmbeddingGenerationService(
modelId: config.DeploymentOrModelId,
apiKey: config.Key,
serviceId: config.Label);
modelId: aiServiceOptions.DeploymentOrModelId,
apiKey: aiServiceOptions.Key,
serviceId: aiServiceOptions.Label);
break;

default:
throw new ArgumentException($"Invalid {nameof(config.AIService)} value in '{AIServiceOptions.EmbeddingPropertyName}' settings.");
throw new ArgumentException($"Invalid {nameof(aiServiceOptions.AIService)} value in '{AIServiceOptions.EmbeddingPropertyName}' settings.");
}

return kernelConfig;
Expand Down
33 changes: 20 additions & 13 deletions samples/apps/copilot-chat-app/webapi/Skills/ChatSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,23 +209,30 @@ public async Task<string> AcquireExternalInformationAsync(SKContext context)

// Create a plan and run it.
Plan plan = await this._planner.CreatePlanAsync(plannerContext.Variables.Input);
SKContext planContext = await plan.InvokeAsync(plannerContext);

// The result of the plan may be from an OpenAPI skill. Attempt to extract JSON from the response.
if (!this.TryExtractJsonFromPlanResult(planContext.Variables.Input, out string planResult))
if (plan.Steps.Count > 0)
{
// If not, use result of the plan execution result directly.
planResult = planContext.Variables.Input;
}
SKContext planContext = await plan.InvokeAsync(plannerContext);

string informationText = $"[START RELATED INFORMATION]\n{planResult.Trim()}\n[END RELATED INFORMATION]\n";
// The result of the plan may be from an OpenAPI skill. Attempt to extract JSON from the response.
if (!this.TryExtractJsonFromPlanResult(planContext.Variables.Input, out string planResult))
{
// If not, use result of the plan execution result directly.
planResult = planContext.Variables.Input;
}

// Adjust the token limit using the number of tokens in the information text.
int tokenLimit = int.Parse(context["tokenLimit"], new NumberFormatInfo());
tokenLimit -= Utilities.TokenCount(informationText);
context.Variables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo()));
string informationText = $"[START RELATED INFORMATION]\n{planResult.Trim()}\n[END RELATED INFORMATION]\n";

return informationText;
// Adjust the token limit using the number of tokens in the information text.
int tokenLimit = int.Parse(context["tokenLimit"], new NumberFormatInfo());
tokenLimit -= Utilities.TokenCount(informationText);
context.Variables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo()));

return informationText;
}
else
{
return string.Empty;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace SemanticKernel.Service.Skills;

/// <summary>
/// A lightweight wrapper around the SequentialPlanner to allow for curating exactly which skills are available to it.
/// A lightweight wrapper around a planner to allow for curating which skills are available to it.
/// </summary>
public class CopilotChatPlanner
{
Expand Down Expand Up @@ -38,7 +38,5 @@ public CopilotChatPlanner(IKernel plannerKernel, IOptions<PlannerOptions> option
/// </summary>
/// <param name="goal">The goal to create a plan for.</param>
/// <returns>The plan.</returns>
public Task<Plan> CreatePlanAsync(string goal)
=> new SequentialPlanner(this.Kernel, this._options.ToSequentialPlannerConfig())
.CreatePlanAsync(goal);
public Task<Plan> CreatePlanAsync(string goal) => new ActionPlanner(this.Kernel).CreatePlanAsync(goal);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"schema_version": "v1",
"name_for_model": "KlarnaProducts",
"name_for_human": "Klarna Shopping",
"description_for_human": "Search and compare prices from thousands of online shops.",
"description_for_model": "Assistant uses the Klarna plugin to get relevant product suggestions for any shopping or product discovery purpose. Assistant will reply with the following 3 paragraphs 1) Search Results 2) Product Comparison of the Search Results 3) Followup Questions. The first paragraph contains a list of the products with their attributes listed clearly and concisely as bullet points under the product, together with a link to the product and an explanation. Links will always be returned and should be shown to the user. The second paragraph compares the results returned in a summary sentence starting with \"In summary\". Assistant comparisons consider only the most important features of the products that will help them fit the users request, and each product mention is brief, short and concise. In the third paragraph assistant always asks helpful follow-up questions and end with a question mark. When assistant is asking a follow-up question, it uses it's product expertise to provide information pertaining to the subject of the user's request that may guide them in their search for the right product.",
"api": {
"type": "openapi",
"url": "https://www.klarna.com/us/shopping/public/openai/v0/api-docs/",
"has_user_authentication": false
},
"auth": {
"type": "none"
},
"logo_url": "https://www.klarna.com/assets/sites/5/2020/04/27143923/klarna-K-150x150.jpg",
"contact_email": "openai-products@klarna.com",
"legal_info_url": "https://www.klarna.com/us/legal/"
}

0 comments on commit 80a43ef

Please sign in to comment.