Skip to content

Commit

Permalink
CopilotChat: Migrate to ActionPlanner (microsoft#765)
Browse files Browse the repository at this point in the history
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.

- 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 authored and codebrain committed May 16, 2023
1 parent 0c66bf1 commit 0ca0e63
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 169 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class PlanData
/// <summary>
/// Parameter values
/// </summary>
public Dictionary<string, string> Parameters { get; set; } = new();
public Dictionary<string, object> Parameters { get; set; } = new();
}

/// <summary>
Expand Down
50 changes: 33 additions & 17 deletions dotnet/src/Extensions/Planning.ActionPlanner/ActionPlanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning.Action;
Expand Down Expand Up @@ -36,19 +38,24 @@ public sealed class ActionPlanner
// Context used to access the list of functions in the kernel
private readonly SKContext _context;
private readonly IKernel _kernel;
private readonly ILogger _logger;

// TODO: allow to inject skill store
/// <summary>
/// Initialize a new instance of the <see cref="ActionPlanner"/> class.
/// </summary>
/// <param name="kernel">The semantic kernel instance.</param>
/// <param name="prompt">Optional prompt override</param>
/// <param name="logger">Optional logger</param>
public ActionPlanner(
IKernel kernel,
string? prompt = null)
string? prompt = null,
ILogger? logger = null)
{
Verify.NotNull(kernel);

this._logger = logger ?? new NullLogger<ActionPlanner>();

string promptTemplate = prompt ?? EmbeddedResource.Read("skprompt.txt");

this._plannerFunction = kernel.CreateSemanticFunction(
Expand All @@ -72,13 +79,10 @@ public async Task<Plan> CreatePlanAsync(string goal)

SKContext result = await this._plannerFunction.InvokeAsync(goal, this._context).ConfigureAwait(false);

var json = """{"plan":{ "rationale":""" + result;

// extract and parse JSON
ActionPlanResponse? planData;
try
{
planData = JsonSerializer.Deserialize<ActionPlanResponse?>(json, new JsonSerializerOptions
planData = JsonSerializer.Deserialize<ActionPlanResponse?>(result.ToString(), new JsonSerializerOptions
{
AllowTrailingCommas = true,
DictionaryKeyPolicy = null,
Expand All @@ -98,35 +102,39 @@ public async Task<Plan> CreatePlanAsync(string goal)
}

// Build and return plan
ISKFunction function;
Plan plan;
if (planData.Plan.Function.Contains("."))
{
var parts = planData.Plan.Function.Split('.');
function = this._context.Skills!.GetFunction(parts[0], parts[1]);
plan = new Plan(goal, this._context.Skills!.GetFunction(parts[0], parts[1]));
}
else if (!string.IsNullOrWhiteSpace(planData.Plan.Function))
{
plan = new Plan(goal, this._context.Skills!.GetFunction(planData.Plan.Function));
}
else
{
function = this._context.Skills!.GetFunction(planData.Plan.Function);
// No function was found - return a plan with no steps.
plan = new Plan(goal);
}

var plan = new Plan(goal);
plan.AddSteps(function);

// Create a plan using the function and the parameters suggested by the planner
var variables = new ContextVariables();
foreach (KeyValuePair<string, string> p in planData.Plan.Parameters)
foreach (KeyValuePair<string, object> p in planData.Plan.Parameters)
{
plan.State[p.Key] = p.Value;
if (p.Value != null)
{
plan.State[p.Key] = p.Value.ToString();
}
}

//Console.WriteLine(JsonSerializer.Serialize(planData, new JsonSerializerOptions { WriteIndented = true }));

var context = this._kernel.CreateNewContext();
context.Variables.Update(variables);

return plan;
}


// TODO: use goal to find relevant functions in a skill store
/// <summary>
/// Native function returning a list of all the functions in the current context,
Expand Down Expand Up @@ -214,7 +222,7 @@ No parameters.
{"plan":{
"rationale": "the list does not contain functions to tell jokes or something funny",
"function": "",
"parameters": {}
"parameters": {
}}}
#END-OF-PLAN
""";
Expand All @@ -230,7 +238,15 @@ private void PopulateList(StringBuilder list, IDictionary<string, List<FunctionV
foreach (FunctionView func in skill.Value)
{
// Function description
list.AppendLine($"// {AddPeriod(func.Description)}");
if (func.Description != null)
{
list.AppendLine($"// {AddPeriod(func.Description)}");
}
else
{
this._logger.LogWarning("{0}.{1} is missing a description.", func.SkillName, func.Name);
list.AppendLine($"// Function {func.SkillName}.{func.Name}.");
}

// Function name
list.AppendLine($"{func.SkillName}.{func.Name}");
Expand Down
4 changes: 1 addition & 3 deletions dotnet/src/Extensions/Planning.ActionPlanner/skprompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,4 @@ For each function the list includes details about the input parameters.
- List of functions:
{{this.ListOfFunctions}}
- End list of functions.
Goal: {{ $input }}
{"plan":{
"rationale":
Goal: {{ $input }}
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);
}
}
}
1 change: 1 addition & 0 deletions samples/apps/copilot-chat-app/webapi/CopilotChatApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

<ItemGroup>
<ProjectReference Include="..\..\..\..\dotnet\src\Connectors\Connectors.AI.OpenAI\Connectors.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Extensions\Planning.ActionPlanner\Planning.ActionPlanner.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" />
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

0 comments on commit 0ca0e63

Please sign in to comment.