Skip to content

Commit f6fb9f9

Browse files
.Net: StepwisePlanner Chat support (#2504)
### Motivation and Context This pull request introduces a series of improvements and updates to the StepwisePlanner, StepwisePlannerTests, and WebSearchEngineSkill. The changes include better handling of action invocation, function retrieval, logging, chat history management, execution helpers, and the addition of an offset parameter to the WebSearchEngineSkill. Additionally, the StepwisePlannerTests have been enhanced with more test cases and refined expected minimum steps, while the ChatRequestSettings class has been improved with a method to create a new settings object from another settings object. Resolves #2367 Fixes #2466 Fixes #2553 ### Description 1. Improved the StepwisePlanner class with better action invocation, function retrieval, logging, chat history management, and execution helpers. 2. Enhanced the StepwisePlannerTests with more test cases and refined expected minimum steps. 3. Added an offset parameter to the WebSearchEngineSkill class for improved search functionality. 4. Improved the ChatRequestSettings class with a method to create a new settings object from another settings object. 5. Updated the Example51_StepwisePlanner.cs file with new questions, improved result handling, and additional output for better analysis. 6. Enhanced the ParseResultTests with new test cases for the ParseResult class. 7. Updated the WebSearchEngineSkill to include parameter descriptions for the SearchAsync method. 8. Added project references and minor changes to the SemanticKernel.MetaPackage project. ### 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: Andrew Hesky @andhesky
1 parent 7313258 commit f6fb9f9

File tree

20 files changed

+892
-367
lines changed

20 files changed

+892
-367
lines changed

dotnet/SK-dotnet.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reliability.Polly", "src\Ex
158158
EndProject
159159
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reliability.Basic", "src\Extensions\Reliability.Basic\Reliability.Basic.csproj", "{3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}"
160160
EndProject
161+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extensions.StepwisePlanner.UnitTests", "src\Extensions\Extensions.StepwisePlanner.UnitTests\Extensions.StepwisePlanner.UnitTests.csproj", "{6F651E87-F16E-407B-AF7F-B3475F850E9A}"
162+
EndProject
161163
Global
162164
GlobalSection(SolutionConfigurationPlatforms) = preSolution
163165
Debug|Any CPU = Debug|Any CPU
@@ -393,6 +395,12 @@ Global
393395
{3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Publish|Any CPU.Build.0 = Publish|Any CPU
394396
{3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Release|Any CPU.ActiveCfg = Release|Any CPU
395397
{3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Release|Any CPU.Build.0 = Release|Any CPU
398+
{6F651E87-F16E-407B-AF7F-B3475F850E9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
399+
{6F651E87-F16E-407B-AF7F-B3475F850E9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
400+
{6F651E87-F16E-407B-AF7F-B3475F850E9A}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
401+
{6F651E87-F16E-407B-AF7F-B3475F850E9A}.Publish|Any CPU.Build.0 = Debug|Any CPU
402+
{6F651E87-F16E-407B-AF7F-B3475F850E9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
403+
{6F651E87-F16E-407B-AF7F-B3475F850E9A}.Release|Any CPU.Build.0 = Release|Any CPU
396404
EndGlobalSection
397405
GlobalSection(SolutionProperties) = preSolution
398406
HideSolutionNode = FALSE
@@ -449,6 +457,7 @@ Global
449457
{10E4B697-D4E8-468D-872D-49670FD150FB} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
450458
{D4540A0F-98E3-4E70-9093-1948AE5B2AAD} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
451459
{3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
460+
{6F651E87-F16E-407B-AF7F-B3475F850E9A} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
452461
EndGlobalSection
453462
GlobalSection(ExtensibilityGlobals) = postSolution
454463
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}

dotnet/samples/KernelSyntaxExamples/Example15_TextMemorySkill.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ private static async Task RunWithStoreAsync(IMemoryStore memoryStore, Cancellati
146146
.WithOpenAITextEmbeddingGenerationService(TestConfiguration.OpenAI.EmbeddingModelId, TestConfiguration.OpenAI.ApiKey)
147147
.Build();
148148

149-
// Create an embedding generator to use for semantic memory.
149+
// Create an embedding generator to use for semantic memory.
150150
var embeddingGenerator = new OpenAITextEmbeddingGeneration(TestConfiguration.OpenAI.EmbeddingModelId, TestConfiguration.OpenAI.ApiKey);
151151

152152
// The combination of the text embedding generator and the memory store makes up the 'SemanticTextMemory' object used to
@@ -155,7 +155,7 @@ private static async Task RunWithStoreAsync(IMemoryStore memoryStore, Cancellati
155155

156156
/////////////////////////////////////////////////////////////////////////////////////////////////////
157157
// PART 1: Store and retrieve memories using the ISemanticTextMemory (textMemory) object.
158-
//
158+
//
159159
// This is a simple way to store memories from a code perspective, without using the Kernel.
160160
/////////////////////////////////////////////////////////////////////////////////////////////////////
161161
Console.WriteLine("== PART 1a: Saving Memories through the ISemanticTextMemory object ==");
@@ -175,12 +175,12 @@ private static async Task RunWithStoreAsync(IMemoryStore memoryStore, Cancellati
175175
// Retrieve a memory
176176
Console.WriteLine("== PART 1b: Retrieving Memories through the ISemanticTextMemory object ==");
177177
MemoryQueryResult? lookup = await textMemory.GetAsync(MemoryCollectionName, "info1", cancellationToken: cancellationToken);
178-
Console.WriteLine("Memory with key 'info3':" + lookup?.Metadata.Text ?? "ERROR: memory not found");
178+
Console.WriteLine("Memory with key 'info1':" + lookup?.Metadata.Text ?? "ERROR: memory not found");
179179
Console.WriteLine();
180180

181181
/////////////////////////////////////////////////////////////////////////////////////////////////////
182182
// PART 2: Create TextMemorySkill, store and retrieve memories through the Kernel.
183-
//
183+
//
184184
// This enables semantic functions and the AI (via Planners) to access memories
185185
/////////////////////////////////////////////////////////////////////////////////////////////////////
186186

@@ -212,8 +212,8 @@ private static async Task RunWithStoreAsync(IMemoryStore memoryStore, Cancellati
212212

213213
/////////////////////////////////////////////////////////////////////////////////////////////////////
214214
// PART 3: Recall similar ideas with semantic search
215-
//
216-
// Uses AI Embeddings for fuzzy lookup of memories based on intent, rather than a specific key.
215+
//
216+
// Uses AI Embeddings for fuzzy lookup of memories based on intent, rather than a specific key.
217217
/////////////////////////////////////////////////////////////////////////////////////////////////////
218218

219219
Console.WriteLine("== PART 3: Recall (similarity search) with AI Embeddings ==");
@@ -260,7 +260,7 @@ private static async Task RunWithStoreAsync(IMemoryStore memoryStore, Cancellati
260260

261261
/////////////////////////////////////////////////////////////////////////////////////////////////////
262262
// PART 3: TextMemorySkill Recall in a Semantic Function
263-
//
263+
//
264264
// Looks up related memories when rendering a prompt template, then sends the rendered prompt to
265265
// the text completion model to answer a natural language query.
266266
/////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -300,7 +300,7 @@ END FACTS
300300

301301
/////////////////////////////////////////////////////////////////////////////////////////////////////
302302
// PART 5: Cleanup, deleting database collection
303-
//
303+
//
304304
/////////////////////////////////////////////////////////////////////////////////////////////////////
305305

306306
Console.WriteLine("== PART 5: Cleanup, deleting database collection ==");

dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs

Lines changed: 168 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System;
4+
using System.Collections.Generic;
45
using System.Diagnostics;
6+
using System.Linq;
57
using System.Threading.Tasks;
68
using Microsoft.SemanticKernel;
9+
using Microsoft.SemanticKernel.Orchestration;
710
using Microsoft.SemanticKernel.Planning;
811
using Microsoft.SemanticKernel.Skills.Core;
912
using Microsoft.SemanticKernel.Skills.Web;
@@ -12,77 +15,216 @@
1215
using RepoUtils;
1316

1417
/**
15-
* This example shows how to use Stepwise Planner to create a plan for a given goal.
18+
* This example shows how to use Stepwise Planner to create and run a stepwise plan for a given goal.
1619
*/
1720

1821
// ReSharper disable once InconsistentNaming
1922
public static class Example51_StepwisePlanner
2023
{
24+
// Used to override the max allowed tokens when running the plan
25+
internal static int? ChatMaxTokens = null;
26+
internal static int? TextMaxTokens = null;
27+
28+
// Used to quickly modify the chat model used by the planner
29+
internal static string? ChatModelOverride = null; //"gpt-35-turbo";
30+
internal static string? TextModelOverride = null; //"text-davinci-003";
31+
32+
internal static string? Suffix = null;
33+
2134
public static async Task RunAsync()
2235
{
2336
string[] questions = new string[]
2437
{
25-
"Who is the current president of the United States? What is his current age divided by 2",
26-
// "Who is Leo DiCaprio's girlfriend? What is her current age raised to the (his current age)/100 power?",
27-
// "What is the capital of France? Who is that city's current mayor? What percentage of their life has been in the 21st century as of today?",
28-
// "What is the current day of the calendar year? Using that as an angle in degrees, what is the area of a unit circle with that angle?"
38+
"What color is the sky?",
39+
"What is the weather in Seattle?",
40+
"What is the tallest mountain on Earth? How tall is it divided by 2?",
41+
"What is the capital of France? Who is that city's current mayor? What percentage of their life has been in the 21st century as of today?",
42+
"What is the current day of the calendar year? Using that as an angle in degrees, what is the area of a unit circle with that angle?",
43+
"If a spacecraft travels at 0.99 the speed of light and embarks on a journey to the nearest star system, Alpha Centauri, which is approximately 4.37 light-years away, how much time would pass on Earth during the spacecraft's voyage?"
2944
};
3045

3146
foreach (var question in questions)
3247
{
33-
var kernel = GetKernel();
34-
await RunWithQuestion(kernel, question);
48+
for (int i = 0; i < 1; i++)
49+
{
50+
await RunTextCompletion(question);
51+
await RunChatCompletion(question);
52+
}
53+
}
54+
55+
PrintResults();
56+
}
57+
58+
// print out summary table of ExecutionResults
59+
private static void PrintResults()
60+
{
61+
Console.WriteLine("**************************");
62+
Console.WriteLine("Execution Results Summary:");
63+
Console.WriteLine("**************************");
64+
65+
foreach (var question in ExecutionResults.Select(s => s.question).Distinct())
66+
{
67+
Console.WriteLine("Question: " + question);
68+
Console.WriteLine("Mode\tModel\tAnswer\tStepsTaken\tIterations\tTimeTaken");
69+
foreach (var er in ExecutionResults.OrderByDescending(s => s.model).Where(s => s.question == question))
70+
{
71+
Console.WriteLine($"{er.mode}\t{er.model}\t{er.stepsTaken}\t{er.iterations}\t{er.timeTaken}\t{er.answer}");
72+
}
3573
}
3674
}
3775

38-
private static async Task RunWithQuestion(IKernel kernel, string question)
76+
private struct ExecutionResult
3977
{
78+
public string mode;
79+
public string? model;
80+
public string? question;
81+
public string? answer;
82+
public string? stepsTaken;
83+
public string? iterations;
84+
public string? timeTaken;
85+
}
86+
87+
private static List<ExecutionResult> ExecutionResults = new();
88+
89+
private static async Task RunTextCompletion(string question)
90+
{
91+
Console.WriteLine("RunTextCompletion");
92+
ExecutionResult currentExecutionResult = default;
93+
currentExecutionResult.mode = "RunTextCompletion";
94+
var kernel = GetKernel(ref currentExecutionResult);
95+
await RunWithQuestion(kernel, currentExecutionResult, question, TextMaxTokens);
96+
}
97+
98+
private static async Task RunChatCompletion(string question, string? model = null)
99+
{
100+
Console.WriteLine("RunChatCompletion");
101+
ExecutionResult currentExecutionResult = default;
102+
currentExecutionResult.mode = "RunChatCompletion";
103+
var kernel = GetKernel(ref currentExecutionResult, true, model);
104+
await RunWithQuestion(kernel, currentExecutionResult, question, ChatMaxTokens);
105+
}
106+
107+
private static async Task RunWithQuestion(IKernel kernel, ExecutionResult currentExecutionResult, string question, int? MaxTokens = null)
108+
{
109+
currentExecutionResult.question = question;
40110
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
41111
var webSearchEngineSkill = new WebSearchEngineSkill(bingConnector);
42112

43113
kernel.ImportSkill(webSearchEngineSkill, "WebSearch");
44-
kernel.ImportSkill(new LanguageCalculatorSkill(kernel), "advancedCalculator");
114+
kernel.ImportSkill(new LanguageCalculatorSkill(kernel), "semanticCalculator");
45115
kernel.ImportSkill(new TimeSkill(), "time");
46116

117+
// StepwisePlanner is instructed to depend on available functions.
118+
// We expose this function to increase the flexibility in it's ability to answer
119+
// given the relatively small number of functions we have in this example.
120+
// This seems to be particularly helpful in these examples with gpt-35-turbo -- even though it
121+
// does not *use* this function. It seems to help the planner find a better path to the answer.
122+
kernel.CreateSemanticFunction(
123+
"Generate an answer for the following question: {{$input}}",
124+
functionName: "GetAnswerForQuestion",
125+
skillName: "AnswerBot",
126+
description: "Given a question, get an answer and return it as the result of the function");
127+
47128
Console.WriteLine("*****************************************************");
48129
Stopwatch sw = new();
49130
Console.WriteLine("Question: " + question);
50131

51132
var plannerConfig = new Microsoft.SemanticKernel.Planning.Stepwise.StepwisePlannerConfig();
52133
plannerConfig.ExcludedFunctions.Add("TranslateMathProblem");
134+
plannerConfig.ExcludedFunctions.Add("DaysAgo");
135+
plannerConfig.ExcludedFunctions.Add("DateMatchingLastDayName");
53136
plannerConfig.MinIterationTimeMs = 1500;
54-
plannerConfig.MaxTokens = 4000;
137+
plannerConfig.MaxIterations = 25;
55138

56-
StepwisePlanner planner = new(kernel, plannerConfig);
57-
sw.Start();
58-
var plan = planner.CreatePlan(question);
139+
if (!string.IsNullOrEmpty(Suffix))
140+
{
141+
plannerConfig.Suffix = $"{Suffix}\n{plannerConfig.Suffix}";
142+
currentExecutionResult.question = $"[Assisted] - {question}";
143+
}
59144

60-
var result = await plan.InvokeAsync(kernel.CreateNewContext());
61-
Console.WriteLine("Result: " + result);
62-
if (result.Variables.TryGetValue("stepCount", out string? stepCount))
145+
if (MaxTokens.HasValue)
63146
{
64-
Console.WriteLine("Steps Taken: " + stepCount);
147+
plannerConfig.MaxTokens = MaxTokens.Value;
65148
}
66149

67-
if (result.Variables.TryGetValue("skillCount", out string? skillCount))
150+
SKContext result;
151+
sw.Start();
152+
153+
try
68154
{
69-
Console.WriteLine("Skills Used: " + skillCount);
155+
StepwisePlanner planner = new(kernel: kernel, config: plannerConfig);
156+
var plan = planner.CreatePlan(question);
157+
158+
result = await plan.InvokeAsync(kernel.CreateNewContext());
159+
160+
if (result.Result.Contains("Result not found, review _stepsTaken to see what", StringComparison.OrdinalIgnoreCase))
161+
{
162+
Console.WriteLine("Could not answer question in " + plannerConfig.MaxIterations + " iterations");
163+
currentExecutionResult.answer = "Could not answer question in " + plannerConfig.MaxIterations + " iterations";
164+
}
165+
else
166+
{
167+
Console.WriteLine("Result: " + result.Result);
168+
currentExecutionResult.answer = result.Result;
169+
}
170+
171+
if (result.Variables.TryGetValue("stepCount", out string? stepCount))
172+
{
173+
Console.WriteLine("Steps Taken: " + stepCount);
174+
currentExecutionResult.stepsTaken = stepCount;
175+
}
176+
177+
if (result.Variables.TryGetValue("skillCount", out string? skillCount))
178+
{
179+
Console.WriteLine("Skills Used: " + skillCount);
180+
}
181+
182+
if (result.Variables.TryGetValue("iterations", out string? iterations))
183+
{
184+
Console.WriteLine("Iterations: " + iterations);
185+
currentExecutionResult.iterations = iterations;
186+
}
187+
}
188+
#pragma warning disable CA1031
189+
catch (Exception ex)
190+
{
191+
Console.WriteLine("Exception: " + ex);
70192
}
71193

72194
Console.WriteLine("Time Taken: " + sw.Elapsed);
195+
currentExecutionResult.timeTaken = sw.Elapsed.ToString();
196+
ExecutionResults.Add(currentExecutionResult);
73197
Console.WriteLine("*****************************************************");
74198
}
75199

76-
private static IKernel GetKernel()
200+
private static IKernel GetKernel(ref ExecutionResult result, bool useChat = false, string? model = null)
77201
{
78202
var builder = new KernelBuilder();
203+
var maxTokens = 0;
204+
if (useChat)
205+
{
206+
builder.WithAzureChatCompletionService(
207+
model ?? ChatModelOverride ?? TestConfiguration.AzureOpenAI.ChatDeploymentName,
208+
TestConfiguration.AzureOpenAI.Endpoint,
209+
TestConfiguration.AzureOpenAI.ApiKey,
210+
alsoAsTextCompletion: true,
211+
setAsDefault: true);
212+
213+
maxTokens = ChatMaxTokens ?? (new Microsoft.SemanticKernel.Planning.Stepwise.StepwisePlannerConfig()).MaxTokens;
214+
result.model = model ?? ChatModelOverride ?? TestConfiguration.AzureOpenAI.ChatDeploymentName;
215+
}
216+
else
217+
{
218+
builder.WithAzureTextCompletionService(
219+
model ?? TextModelOverride ?? TestConfiguration.AzureOpenAI.DeploymentName,
220+
TestConfiguration.AzureOpenAI.Endpoint,
221+
TestConfiguration.AzureOpenAI.ApiKey);
79222

80-
builder.WithAzureChatCompletionService(
81-
TestConfiguration.AzureOpenAI.ChatDeploymentName,
82-
TestConfiguration.AzureOpenAI.Endpoint,
83-
TestConfiguration.AzureOpenAI.ApiKey,
84-
alsoAsTextCompletion: true,
85-
setAsDefault: true);
223+
maxTokens = TextMaxTokens ?? (new Microsoft.SemanticKernel.Planning.Stepwise.StepwisePlannerConfig()).MaxTokens;
224+
result.model = model ?? TextModelOverride ?? TestConfiguration.AzureOpenAI.DeploymentName;
225+
}
226+
227+
Console.WriteLine($"Model: {result.model} ({maxTokens})");
86228

87229
var kernel = builder
88230
.WithLoggerFactory(ConsoleLogger.LoggerFactory)
@@ -97,32 +239,3 @@ private static IKernel GetKernel()
97239
return kernel;
98240
}
99241
}
100-
101-
// *****************************************************
102-
// Question: Who is the current president of the United States? What is his current age divided by 2
103-
// Result: The current president of the United States is Joe Biden. His current age divided by 2 is 40.5.
104-
// Steps Taken: 9
105-
// Skills Used: 7 (WebSearch.Search(4), time.Year(1), time.Date(1), advancedCalculator.Calculator(1))
106-
// Time Taken: 00:01:13.3766860
107-
// *****************************************************
108-
// *****************************************************
109-
// Question: Who is Leo DiCaprio's girlfriend? What is her current age raised to the (his current age)/100 power?
110-
// Result: Leo DiCaprio's girlfriend is Camila Morrone. Her current age raised to the power of (his current age)/100 is approximately 4.94.
111-
// Steps Taken: 9
112-
// Skills Used: 5 (WebSearch.Search(3), time.Year(1), advancedCalculator.Calculator(1))
113-
// Time Taken: 00:01:17.6742136
114-
// *****************************************************
115-
// *****************************************************
116-
// Question: What is the capital of France? Who is that cities current mayor? What percentage of their life has been in the 21st century as of today?
117-
// Result: The capital of France is Paris. The current mayor of Paris is Anne Hidalgo, who was born on June 19, 1959. As of today, she has lived for 64 years, with 23 of those years in the 21st century. Therefore, 35.94% of her life has been spent in the 21st century.
118-
// Steps Taken: 14
119-
// Skills Used: 12 (WebSearch.Search(8), time.Year(1), advancedCalculator.Calculator(3))
120-
// Time Taken: 00:02:06.6682909
121-
// *****************************************************
122-
// *****************************************************
123-
// Question: What is the current day of the calendar year? Using that as an angle in degrees, what is the area of a unit circle with that angle?
124-
// Result: The current day of the year is 177. Using that as an angle in degrees (approximately 174.58), the area of a unit circle with that angle is approximately 1.523 square units.
125-
// Steps Taken: 11
126-
// Skills Used: 9 (time.Now(1), time.DayOfYear(1), time.DaysBetween(1), time.MonthNumber(1), time.Day(1), advancedCalculator.Calculator(4))
127-
// Time Taken: 00:01:41.5585861
128-
// *****************************************************

0 commit comments

Comments
 (0)