diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index 50c300e747f5..127abd47b87b 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -150,6 +150,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Skills.Core", "src\Skills\S
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCalcSkills", "samples\NCalcSkills\NCalcSkills.csproj", "{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Planning.StepwisePlanner", "src\Extensions\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj", "{4762BCAF-E1C5-4714-B88D-E50FA333C50E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -375,6 +377,12 @@ Global
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Publish|Any CPU.Build.0 = Debug|Any CPU
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
+ {4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Publish|Any CPU.Build.0 = Publish|Any CPU
+ {4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -429,6 +437,7 @@ Global
{185E0CE8-C2DA-4E4C-A491-E8EB40316315} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA} = {FA3720F1-C99A-49B2-9577-A940257098BF}
+ {4762BCAF-E1C5-4714-B88D-E50FA333C50E} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs b/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs
new file mode 100644
index 000000000000..780e4f14ab27
--- /dev/null
+++ b/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs
@@ -0,0 +1,189 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Planning;
+using Microsoft.SemanticKernel.Reliability;
+using Microsoft.SemanticKernel.Skills.Core;
+using Microsoft.SemanticKernel.Skills.Web;
+using Microsoft.SemanticKernel.Skills.Web.Bing;
+using NCalcSkills;
+using RepoUtils;
+
+/**
+ * This example shows how to use Stepwise Planner to create a plan for a given goal.
+ */
+
+// ReSharper disable once InconsistentNaming
+public static class Example51_StepwisePlanner
+{
+ public static async Task RunAsync()
+ {
+ string[] questions = new string[]
+ {
+ "Who is the current president of the United States? What is his current age divided by 2",
+ // "Who is Leo DiCaprio's girlfriend? What is her current age raised to the (his current age)/100 power?",
+ // "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?",
+ // "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?"
+ };
+
+ foreach (var question in questions)
+ {
+ await RunTextCompletion(question);
+ await RunChatCompletion(question);
+ }
+ }
+
+ public static async Task RunTextCompletion(string question)
+ {
+ Console.WriteLine("RunTextCompletion");
+ var kernel = GetKernel();
+ await RunWithQuestion(kernel, question);
+ }
+
+ public static async Task RunChatCompletion(string question)
+ {
+ Console.WriteLine("RunChatCompletion");
+ var kernel = GetKernel(true);
+ await RunWithQuestion(kernel, question);
+ }
+
+ public static async Task RunWithQuestion(IKernel kernel, string question)
+ {
+ using var bingConnector = new BingConnector(Env.Var("BING_API_KEY"));
+ var webSearchEngineSkill = new WebSearchEngineSkill(bingConnector);
+
+ kernel.ImportSkill(webSearchEngineSkill, "WebSearch");
+ kernel.ImportSkill(new LanguageCalculatorSkill(kernel), "advancedCalculator");
+ // kernel.ImportSkill(new SimpleCalculatorSkill(kernel), "basicCalculator");
+ kernel.ImportSkill(new TimeSkill(), "time");
+
+ Console.WriteLine("*****************************************************");
+ Stopwatch sw = new();
+ Console.WriteLine("Question: " + question);
+
+ var config = new Microsoft.SemanticKernel.Planning.Stepwise.StepwisePlannerConfig();
+ config.ExcludedFunctions.Add("TranslateMathProblem");
+ config.MinIterationTimeMs = 1500;
+ config.MaxTokens = 4000;
+
+ StepwisePlanner planner = new(kernel, config);
+ sw.Start();
+ var plan = planner.CreatePlan(question);
+
+ var result = await plan.InvokeAsync(kernel.CreateNewContext());
+ Console.WriteLine("Result: " + result);
+ if (result.Variables.TryGetValue("stepCount", out string? stepCount))
+ {
+ Console.WriteLine("Steps Taken: " + stepCount);
+ }
+
+ if (result.Variables.TryGetValue("skillCount", out string? skillCount))
+ {
+ Console.WriteLine("Skills Used: " + skillCount);
+ }
+
+ Console.WriteLine("Time Taken: " + sw.Elapsed);
+ Console.WriteLine("*****************************************************");
+ }
+
+ private static IKernel GetKernel(bool useChat = false)
+ {
+ var builder = new KernelBuilder();
+ if (useChat)
+ {
+ builder.WithAzureChatCompletionService(
+ Env.Var("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"),
+ Env.Var("AZURE_OPENAI_ENDPOINT"),
+ Env.Var("AZURE_OPENAI_KEY"),
+ alsoAsTextCompletion: true,
+ setAsDefault: true);
+ }
+ else
+ {
+ builder.WithAzureTextCompletionService(
+ Env.Var("AZURE_OPENAI_DEPLOYMENT_NAME"),
+ Env.Var("AZURE_OPENAI_ENDPOINT"),
+ Env.Var("AZURE_OPENAI_KEY"));
+ }
+
+ var kernel = builder
+ .WithLogger(ConsoleLogger.Log)
+ .Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig
+ {
+ MaxRetryCount = 3,
+ UseExponentialBackoff = true,
+ MinRetryDelay = TimeSpan.FromSeconds(3),
+ }))
+ .Build();
+
+ return kernel;
+ }
+}
+
+// RunTextCompletion
+// *****************************************************
+// Question: Who is the current president of the United States? What is his current age divided by 2
+// Result: The current president of the United States is Joe Biden. His current age divided by 2 is 40.
+// Steps Taken: 10
+// Skills Used: 4 (WebSearch.Search(2), time.Date(1), advancedCalculator.Calculator(1))
+// Time Taken: 00:00:53.6331324
+// *****************************************************
+// RunChatCompletion
+// *****************************************************
+// Question: Who is the current president of the United States? What is his current age divided by 2
+// Result: The current president of the United States is Joe Biden. His current age divided by 2 is 40.5.
+// Steps Taken: 9
+// Skills Used: 7 (WebSearch.Search(4), time.Year(1), time.Date(1), advancedCalculator.Calculator(1))
+// Time Taken: 00:01:13.3766860
+// *****************************************************
+// RunTextCompletion
+// *****************************************************
+// Question: Who is Leo DiCaprio's girlfriend? What is her current age raised to the (his current age)/100 power?
+// Result: Leo DiCaprio's girlfriend is Camila Morrone. Her current age raised to the (his current age)/100 power is 4.935565735151678.
+// Steps Taken: 6
+// Skills Used: 5 (WebSearch.Search(3), time.Year(1), advancedCalculator.Calculator(1))
+// Time Taken: 00:00:37.8941510
+// *****************************************************
+// RunChatCompletion
+// *****************************************************
+// Question: Who is Leo DiCaprio's girlfriend? What is her current age raised to the (his current age)/100 power?
+// Result: Leo DiCaprio's girlfriend is Camila Morrone. Her current age raised to the power of (his current age)/100 is approximately 4.94.
+// Steps Taken: 9
+// Skills Used: 5 (WebSearch.Search(3), time.Year(1), advancedCalculator.Calculator(1))
+// Time Taken: 00:01:17.6742136
+// *****************************************************
+// RunTextCompletion
+// *****************************************************
+// 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?
+// Result: The capital of France is Paris. The current mayor of Paris is Anne Hidalgo. She has spent 36.51% of her life in the 21st century as of 2023.
+// Steps Taken: 7
+// Skills Used: 4 (WebSearch.Search(3), advancedCalculator.Calculator(1))
+// Time Taken: 00:00:41.6837628
+// *****************************************************
+// RunChatCompletion
+// *****************************************************
+// 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?
+// 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.
+// Steps Taken: 14
+// Skills Used: 12 (WebSearch.Search(8), time.Year(1), advancedCalculator.Calculator(3))
+// Time Taken: 00:02:06.6682909
+// *****************************************************
+// RunTextCompletion
+// *****************************************************
+// 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?
+// Result: The current day of the calendar year is 177. The angle in degrees corresponding to this day is 174.6. The area of a unit circle with that angle is 0.764 * pi.
+// Steps Taken: 16
+// Skills Used: 2 (time.DayOfYear(1), time.Date(1))
+// Time Taken: 00:01:29.9931039
+// *****************************************************
+// RunChatCompletion
+// *****************************************************
+// 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?
+// 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.
+// Steps Taken: 11
+// Skills Used: 9 (time.Now(1), time.DayOfYear(1), time.DaysBetween(1), time.MonthNumber(1), time.Day(1), advancedCalculator.Calculator(4))
+// Time Taken: 00:01:41.5585861
+// *****************************************************
diff --git a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
index 46b4d5046476..996ea3b9b97d 100644
--- a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
+++ b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj
@@ -1,4 +1,4 @@
-
+5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
@@ -35,6 +35,7 @@
+
@@ -42,6 +43,7 @@
+
diff --git a/dotnet/samples/KernelSyntaxExamples/Program.cs b/dotnet/samples/KernelSyntaxExamples/Program.cs
index 3aef36d292a8..cfba43232baf 100644
--- a/dotnet/samples/KernelSyntaxExamples/Program.cs
+++ b/dotnet/samples/KernelSyntaxExamples/Program.cs
@@ -157,5 +157,8 @@ public static async Task Main()
await Example50_Chroma.RunAsync();
Console.WriteLine("== DONE ==");
+
+ await Example51_StepwisePlanner.RunAsync();
+ Console.WriteLine("== DONE ==");
}
}
diff --git a/dotnet/samples/NCalcSkills/NCalcSkills.csproj b/dotnet/samples/NCalcSkills/NCalcSkills.csproj
index 7d2ff2bc0db7..88adf3e9190f 100644
--- a/dotnet/samples/NCalcSkills/NCalcSkills.csproj
+++ b/dotnet/samples/NCalcSkills/NCalcSkills.csproj
@@ -1,8 +1,4 @@
-
- $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)'))))
-
-
netstandard2.010
@@ -12,13 +8,7 @@
-
- $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)'))))
-
-
-
-
diff --git a/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj b/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj
index 561dcfacd85d..27580b44a75b 100644
--- a/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj
+++ b/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj
@@ -30,6 +30,7 @@
+
diff --git a/dotnet/src/Extensions/Extensions.UnitTests/Planning/StepwisePlanner/ParseResultTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/Planning/StepwisePlanner/ParseResultTests.cs
new file mode 100644
index 000000000000..589d50555313
--- /dev/null
+++ b/dotnet/src/Extensions/Extensions.UnitTests/Planning/StepwisePlanner/ParseResultTests.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.SkillDefinition;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Extensions.UnitTests.Planning.StepwisePlanner;
+
+public sealed class ParseResultTests
+{
+ [Theory]
+ [InlineData("[FINAL ANSWER] 42", "42")]
+ [InlineData("[FINAL ANSWER]42", "42")]
+ [InlineData("I think I have everything I need.\n[FINAL ANSWER] 42", "42")]
+ [InlineData("I think I have everything I need.\n[FINAL ANSWER] 42\n", "42")]
+ [InlineData("I think I have everything I need.\n[FINAL ANSWER] 42\n\n", "42")]
+ [InlineData("I think I have everything I need.\n[FINAL ANSWER]42\n\n\n", "42")]
+ [InlineData("I think I have everything I need.\n[FINAL ANSWER]\n 42\n\n\n", "42")]
+ public void WhenInputIsFinalAnswerReturnsFinalAnswer(string input, string expected)
+ {
+ // Arrange
+ var kernel = new Mock();
+ kernel.Setup(x => x.Log).Returns(new Mock().Object);
+
+ var planner = new Microsoft.SemanticKernel.Planning.StepwisePlanner(kernel.Object);
+
+ // Act
+ var result = planner.ParseResult(input);
+
+ // Assert
+ Assert.Equal(expected, result.FinalAnswer);
+ }
+
+ [Theory]
+ [InlineData("To answer the first part of the question, I need to search for Leo DiCaprio's girlfriend on the web. To answer the second part, I need to find her current age and use a calculator to raise it to the 0.43 power.\n[ACTION]\n{\n \"action\": \"Search\",\n \"action_variables\": {\"input\": \"Leo DiCaprio's girlfriend\"}\n}", "Search", "input", "Leo DiCaprio's girlfriend")]
+ [InlineData("To answer the first part of the question, I need to search the web for Leo DiCaprio's girlfriend. To answer the second part, I need to find her current age and use the calculator tool to raise it to the 0.43 power.\n[ACTION]\n```\n{\n \"action\": \"Search\",\n \"action_variables\": {\"input\": \"Leo DiCaprio's girlfriend\"}\n}\n```", "Search", "input", "Leo DiCaprio's girlfriend")]
+ [InlineData("The web search result is a snippet from a Wikipedia article that says Leo DiCaprio's girlfriend is Camila Morrone, an Argentine-American model and actress. I need to find out her current age, which might be in the same article or another source. I can use the WebSearch.Search function again to search for her name and age.\n\n[ACTION] {\n \"action\": \"WebSearch.Search\",\n \"action_variables\": {\"input\": \"Camila Morrone age\", \"count\": \"1\"}\n}", "WebSearch.Search", "input",
+ "Camila Morrone age", "count", "1")]
+ public void ParseActionReturnsAction(string input, string expectedAction, params string[] expectedVariables)
+ {
+ Dictionary? expectedDictionary = null;
+ for (int i = 0; i < expectedVariables.Length; i += 2)
+ {
+ expectedDictionary ??= new Dictionary();
+ expectedDictionary.Add(expectedVariables[i], expectedVariables[i + 1]);
+ }
+
+ // Arrange
+ var kernel = new Mock();
+ kernel.Setup(x => x.Log).Returns(new Mock().Object);
+
+ var planner = new Microsoft.SemanticKernel.Planning.StepwisePlanner(kernel.Object);
+
+ // Act
+ var result = planner.ParseResult(input);
+
+ // Assert
+ Assert.Equal(expectedAction, result.Action);
+ Assert.Equal(expectedDictionary, result.ActionVariables);
+ }
+
+ // Method to create Mock objects
+ private static Mock CreateMockFunction(FunctionView functionView)
+ {
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.Describe()).Returns(functionView);
+ mockFunction.Setup(x => x.Name).Returns(functionView.Name);
+ mockFunction.Setup(x => x.SkillName).Returns(functionView.SkillName);
+ return mockFunction;
+ }
+}
diff --git a/dotnet/src/Extensions/Planning.StepwisePlanner/EmbeddedResource.cs b/dotnet/src/Extensions/Planning.StepwisePlanner/EmbeddedResource.cs
new file mode 100644
index 000000000000..9e8a711bdb9c
--- /dev/null
+++ b/dotnet/src/Extensions/Planning.StepwisePlanner/EmbeddedResource.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.IO;
+using System.Reflection;
+
+namespace Microsoft.SemanticKernel.Planning.Stepwise;
+
+internal static class EmbeddedResource
+{
+ private static readonly string? s_namespace = typeof(EmbeddedResource).Namespace;
+
+ internal static string Read(string name)
+ {
+ var assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
+ if (assembly == null) { throw new PlanningException(PlanningException.ErrorCodes.InvalidConfiguration, $"[{s_namespace}] {name} assembly not found"); }
+
+ using Stream? resource = assembly.GetManifestResourceStream($"{s_namespace}." + name);
+ if (resource == null) { throw new PlanningException(PlanningException.ErrorCodes.InvalidConfiguration, $"[{s_namespace}] {name} resource not found"); }
+
+ using var reader = new StreamReader(resource);
+ return reader.ReadToEnd();
+ }
+}
diff --git a/dotnet/src/Extensions/Planning.StepwisePlanner/Planning.StepwisePlanner.csproj b/dotnet/src/Extensions/Planning.StepwisePlanner/Planning.StepwisePlanner.csproj
new file mode 100644
index 000000000000..accf224de931
--- /dev/null
+++ b/dotnet/src/Extensions/Planning.StepwisePlanner/Planning.StepwisePlanner.csproj
@@ -0,0 +1,31 @@
+
+
+
+
+ Microsoft.SemanticKernel.Planning.StepwisePlanner
+ Microsoft.SemanticKernel.Planning.Stepwise
+ netstandard2.0
+
+
+
+
+
+
+
+ Semantic Kernel - Stepwise Planner
+ Semantic Kernel Stepwise Planner
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/dotnet/src/Extensions/Planning.StepwisePlanner/Skills/StepwiseStep/config.json b/dotnet/src/Extensions/Planning.StepwisePlanner/Skills/StepwiseStep/config.json
new file mode 100644
index 000000000000..51ef104e4597
--- /dev/null
+++ b/dotnet/src/Extensions/Planning.StepwisePlanner/Skills/StepwiseStep/config.json
@@ -0,0 +1,32 @@
+{
+ "schema": 1,
+ "description": "Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.",
+ "type": "completion",
+ "completion": {
+ "max_tokens": 1024,
+ "temperature": 0,
+ "top_p": 0,
+ "presence_penalty": 0,
+ "frequency_penalty": 0,
+ "stop_sequences": ["[OBSERVATION]", "\n[THOUGHT]"]
+ },
+ "input": {
+ "parameters": [
+ {
+ "name": "question",
+ "description": "The question to answer",
+ "defaultValue": ""
+ },
+ {
+ "name": "agentScratchPad",
+ "description": "The agent's scratch pad",
+ "defaultValue": ""
+ },
+ {
+ "name": "functionDescriptions",
+ "description": "The manual of the agent's functions",
+ "defaultValue": ""
+ }
+ ]
+ }
+}
diff --git a/dotnet/src/Extensions/Planning.StepwisePlanner/Skills/StepwiseStep/skprompt.txt b/dotnet/src/Extensions/Planning.StepwisePlanner/Skills/StepwiseStep/skprompt.txt
new file mode 100644
index 000000000000..723b68d74c6a
--- /dev/null
+++ b/dotnet/src/Extensions/Planning.StepwisePlanner/Skills/StepwiseStep/skprompt.txt
@@ -0,0 +1,49 @@
+[INSTRUCTION]
+Answer the following questions as accurately as possible using the provided functions.
+
+[AVAILABLE FUNCTIONS]
+The function definitions below are in the following format:
+:
+ - :
+ - ...
+
+{{$functionDescriptions}}
+[END AVAILABLE FUNCTIONS]
+
+[USAGE INSTRUCTIONS]
+To use the functions, specify a JSON blob representing an action. The JSON blob should contain an "action" key with the name of the function to use, and an "action_variables" key with a JSON object of string values to use when calling the function.
+Do not call functions directly; they must be invoked through an action.
+The "action_variables" value should always include an "input" key, even if the input value is empty. Additional keys in the "action_variables" value should match the defined [PARAMETERS] of the named "action" in [AVAILABLE FUNCTIONS].
+Dictionary values in "action_variables" must be strings and represent the actual values to be passed to the function.
+Ensure that the $JSON_BLOB contains only a SINGLE action; do NOT return multiple actions.
+IMPORTANT: Use only the available functions listed in the [AVAILABLE FUNCTIONS] section. Do not attempt to use any other functions that are not specified.
+
+Here is an example of a valid $JSON_BLOB:
+{
+ "action": "FUNCTION.NAME",
+ "action_variables": {"INPUT": "some input", "PARAMETER_NAME": "some value", "PARAMETER_NAME_2": "42"}
+}
+[END USAGE INSTRUCTIONS]
+[END INSTRUCTION]
+
+[THOUGHT PROCESS]
+[QUESTION]
+the input question I must answer
+[THOUGHT]
+To solve this problem, I should carefully analyze the given question and identify the necessary steps. Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
+[ACTION]
+$JSON_BLOB
+[OBSERVATION]
+The result of the action will be provided here.
+... (These Thought/Action/Observation can repeat until the final answer is reached.)
+[FINAL ANSWER]
+Once I have gathered all the necessary observations and performed any required actions, I can provide the final answer in a clear and human-readable format.
+[END THOUGHT PROCESS]
+
+Let's break down the problem step by step and think about the best approach. Questions and observations should be followed by a single thought and an optional single action to take.
+
+Begin!
+
+[QUESTION]
+{{$question}}
+{{$agentScratchPad}}
diff --git a/dotnet/src/Extensions/Planning.StepwisePlanner/StepwisePlanner.cs b/dotnet/src/Extensions/Planning.StepwisePlanner/StepwisePlanner.cs
new file mode 100644
index 000000000000..b108b88b366f
--- /dev/null
+++ b/dotnet/src/Extensions/Planning.StepwisePlanner/StepwisePlanner.cs
@@ -0,0 +1,482 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.Diagnostics;
+using Microsoft.SemanticKernel.Orchestration;
+using Microsoft.SemanticKernel.Planning.Stepwise;
+using Microsoft.SemanticKernel.SemanticFunctions;
+using Microsoft.SemanticKernel.SkillDefinition;
+
+#pragma warning disable IDE0130
+// ReSharper disable once CheckNamespace - Using NS of Plan
+namespace Microsoft.SemanticKernel.Planning;
+#pragma warning restore IDE0130
+
+///
+/// A planner that creates a Stepwise plan using Mrkl systems.
+///
+///
+/// An implementation of a Mrkl system as described in https://arxiv.org/pdf/2205.00445.pdf
+///
+public class StepwisePlanner
+{
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ /// The semantic kernel instance.
+ /// Optional configuration object
+ /// Optional prompt override
+ public StepwisePlanner(
+ IKernel kernel,
+ StepwisePlannerConfig? config = null,
+ string? prompt = null)
+ {
+ Verify.NotNull(kernel);
+ this._kernel = kernel;
+
+ this.Config = config ?? new();
+ this.Config.ExcludedSkills.Add(RestrictedSkillName);
+
+ var promptConfig = new PromptTemplateConfig();
+ var promptTemplate = prompt ?? EmbeddedResource.Read("Skills.StepwiseStep.skprompt.txt");
+ string promptConfigString = EmbeddedResource.Read("Skills.StepwiseStep.config.json");
+ if (!string.IsNullOrEmpty(promptConfigString))
+ {
+ promptConfig = PromptTemplateConfig.FromJson(promptConfigString);
+ }
+
+ promptConfig.Completion.MaxTokens = this.Config.MaxTokens;
+
+ this._systemStepFunction = this.ImportSemanticFunction(this._kernel, "StepwiseStep", promptTemplate, promptConfig);
+ this._nativeFunctions = this._kernel.ImportSkill(this, RestrictedSkillName);
+
+ this._context = this._kernel.CreateNewContext();
+ this._logger = this._kernel.Log;
+ }
+
+ public Plan CreatePlan(string goal)
+ {
+ if (string.IsNullOrEmpty(goal))
+ {
+ throw new PlanningException(PlanningException.ErrorCodes.InvalidGoal, "The goal specified is empty");
+ }
+
+ string functionDescriptions = this.GetFunctionDescriptions();
+
+ Plan planStep = new(this._nativeFunctions["ExecutePlan"]);
+ planStep.Parameters.Set("functionDescriptions", functionDescriptions);
+ planStep.Parameters.Set("question", goal);
+
+ planStep.Outputs.Add("agentScratchPad");
+ planStep.Outputs.Add("stepCount");
+ planStep.Outputs.Add("skillCount");
+ planStep.Outputs.Add("stepsTaken");
+
+ Plan plan = new(goal);
+
+ plan.AddSteps(planStep);
+
+ return plan;
+ }
+
+ [SKFunction, SKName("ExecutePlan"), Description("Execute a plan")]
+ public async Task ExecutePlanAsync(
+ [Description("The question to answer")]
+ string question,
+ [Description("List of tool descriptions")]
+ string functionDescriptions,
+ SKContext context)
+ {
+ var stepsTaken = new List();
+ this._logger?.BeginScope("StepwisePlanner");
+ if (!string.IsNullOrEmpty(question))
+ {
+ this._logger?.LogInformation("Ask: {Question}", question);
+ for (int i = 0; i < this.Config.MaxIterations; i++)
+ {
+ var scratchPad = this.CreateScratchPad(question, stepsTaken);
+ this._logger?.LogDebug("Scratchpad: {ScratchPad}", scratchPad);
+ context.Variables.Set("agentScratchPad", scratchPad);
+
+ var llmResponse = (await this._systemStepFunction.InvokeAsync(context).ConfigureAwait(false));
+
+ if (llmResponse.ErrorOccurred)
+ {
+ var exception = new PlanningException(PlanningException.ErrorCodes.UnknownError, $"Error occurred while executing stepwise plan: {llmResponse.LastErrorDescription}", llmResponse.LastException);
+ context.Fail(exception.Message, exception);
+ return context;
+ }
+
+ string actionText = llmResponse.Result.Trim();
+ this._logger?.LogDebug("Response : {ActionText}", actionText);
+
+ var nextStep = this.ParseResult(actionText);
+ stepsTaken.Add(nextStep);
+
+ if (!string.IsNullOrEmpty(nextStep.FinalAnswer))
+ {
+ this._logger?.LogInformation("Final Answer: {FinalAnswer}", nextStep.FinalAnswer);
+ context.Variables.Update(nextStep.FinalAnswer);
+ var updatedScratchPlan = this.CreateScratchPad(question, stepsTaken);
+ context.Variables.Set("agentScratchPad", updatedScratchPlan);
+
+ // Add additional results to the context
+ this.AddExecutionStatsToContext(stepsTaken, context);
+
+ return context;
+ }
+
+ this._logger?.LogInformation("Thought: {Thought}", nextStep.Thought);
+
+ if (!string.IsNullOrEmpty(nextStep!.Action!))
+ {
+ this._logger?.LogInformation("Action: {Action}({ActionVariables})", nextStep.Action, JsonSerializer.Serialize(nextStep.ActionVariables));
+ try
+ {
+ await Task.Delay(this.Config.MinIterationTimeMs).ConfigureAwait(false);
+ var result = await this.InvokeActionAsync(nextStep.Action!, nextStep!.ActionVariables!).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(result))
+ {
+ nextStep.Observation = "Got no result from action";
+ }
+ else
+ {
+ nextStep.Observation = result;
+ }
+ }
+ catch (Exception ex) when (!ex.IsCriticalException())
+ {
+ nextStep.Observation = $"Error invoking action {nextStep.Action} : {ex.Message}";
+ this._logger?.LogDebug(ex, "Error invoking action {Action}", nextStep.Action);
+ }
+
+ this._logger?.LogInformation("Observation: {Observation}", nextStep.Observation);
+ }
+ else
+ {
+ this._logger?.LogInformation("Action: No action to take");
+ }
+
+ // sleep 3 seconds
+ await Task.Delay(this.Config.MinIterationTimeMs).ConfigureAwait(false);
+ }
+
+ context.Variables.Update($"Result not found, review _stepsTaken to see what happened.\n{JsonSerializer.Serialize(stepsTaken)}");
+ }
+ else
+ {
+ context.Variables.Update("Question not found.");
+ }
+
+ return context;
+ }
+
+ public virtual SystemStep ParseResult(string input)
+ {
+ var result = new SystemStep
+ {
+ OriginalResponse = input
+ };
+
+ // Extract final answer
+ Match finalAnswerMatch = s_finalAnswerRegex.Match(input);
+
+ if (finalAnswerMatch.Success)
+ {
+ result.FinalAnswer = finalAnswerMatch.Groups[1].Value.Trim();
+ return result;
+ }
+
+ // Extract thought
+ Match thoughtMatch = s_thoughtRegex.Match(input);
+
+ if (thoughtMatch.Success)
+ {
+ result.Thought = thoughtMatch.Value.Trim();
+ }
+ else if (!input.Contains(Action))
+ {
+ result.Thought = input;
+ }
+ else
+ {
+ throw new InvalidOperationException("Unexpected input format");
+ }
+
+ result.Thought = result.Thought.Replace(Thought, string.Empty).Trim();
+
+ // Extract action
+ Match actionMatch = s_actionRegex.Match(input);
+
+ if (actionMatch.Success)
+ {
+ var json = actionMatch.Groups[1].Value.Trim();
+
+ try
+ {
+ var systemStepResults = JsonSerializer.Deserialize(json);
+
+ if (systemStepResults == null)
+ {
+ result.Observation = $"System step parsing error, empty JSON: {json}";
+ }
+ else
+ {
+ result.Action = systemStepResults.Action;
+ result.ActionVariables = systemStepResults.ActionVariables;
+ }
+ }
+ catch (JsonException)
+ {
+ result.Observation = $"System step parsing error, invalid JSON: {json}";
+ }
+ }
+
+ if (string.IsNullOrEmpty(result.Thought) && string.IsNullOrEmpty(result.Action))
+ {
+ result.Observation = "System step error, no thought or action found. Please give a valid thought and/or action.";
+ }
+
+ return result;
+ }
+
+ private void AddExecutionStatsToContext(List stepsTaken, SKContext context)
+ {
+ context.Variables.Set("stepCount", stepsTaken.Count.ToString(CultureInfo.InvariantCulture));
+ context.Variables.Set("stepsTaken", JsonSerializer.Serialize(stepsTaken));
+
+ Dictionary actionCounts = new();
+ foreach (var step in stepsTaken)
+ {
+ if (string.IsNullOrEmpty(step.Action)) { continue; }
+
+ _ = actionCounts.TryGetValue(step.Action!, out int currentCount);
+ actionCounts[step.Action!] = ++currentCount;
+ }
+
+ var skillCallListWithCounts = string.Join(", ", actionCounts.Keys.Select(skill =>
+ $"{skill}({actionCounts[skill]})"));
+
+ var skillCallCountStr = actionCounts.Values.Sum().ToString(CultureInfo.InvariantCulture);
+
+ context.Variables.Set("skillCount", $"{skillCallCountStr} ({skillCallListWithCounts})");
+ }
+
+ private string CreateScratchPad(string question, List stepsTaken)
+ {
+ if (stepsTaken.Count == 0)
+ {
+ return string.Empty;
+ }
+
+ var scratchPadLines = new List();
+
+ // Add the original first thought
+ scratchPadLines.Add(ScratchPadPrefix);
+ scratchPadLines.Add($"{Thought} {stepsTaken[0].Thought}");
+
+ // Keep track of where to insert the next step
+ var insertPoint = scratchPadLines.Count;
+
+ // Keep the most recent steps in the scratch pad.
+ for (var i = stepsTaken.Count - 1; i >= 0; i--)
+ {
+ if (scratchPadLines.Count / 4.0 > (this.Config.MaxTokens * 0.75))
+ {
+ this._logger.LogDebug("Scratchpad is too long, truncating. Skipping {CountSkipped} steps.", i + 1);
+ break;
+ }
+
+ var s = stepsTaken[i];
+
+ if (!string.IsNullOrEmpty(s.Observation))
+ {
+ scratchPadLines.Insert(insertPoint, $"{Observation} {s.Observation}");
+ }
+
+ if (!string.IsNullOrEmpty(s.Action))
+ {
+ scratchPadLines.Insert(insertPoint, $"{Action} {{\"action\": \"{s.Action}\",\"action_variables\": {JsonSerializer.Serialize(s.ActionVariables)}}}");
+ }
+
+ if (i != 0)
+ {
+ scratchPadLines.Insert(insertPoint, $"{Thought} {s.Thought}");
+ }
+ }
+
+ return string.Join("\n", scratchPadLines).Trim();
+ }
+
+ private async Task InvokeActionAsync(string actionName, Dictionary actionVariables)
+ {
+ var availableFunctions = this.GetAvailableFunctions();
+ var targetFunction = availableFunctions.FirstOrDefault(f => ToFullyQualifiedName(f) == actionName);
+ if (targetFunction == null)
+ {
+ throw new PlanningException(PlanningException.ErrorCodes.UnknownError, $"The function '{actionName}' was not found.");
+ }
+
+ try
+ {
+ var function = this._kernel.Func(targetFunction.SkillName, targetFunction.Name);
+ var actionContext = this.CreateActionContext(actionVariables);
+
+ var result = await function.InvokeAsync(actionContext).ConfigureAwait(false);
+
+ if (result.ErrorOccurred)
+ {
+ this._logger?.LogError("Error occurred: {Error}", result.LastException);
+ return $"Error occurred: {result.LastException}";
+ }
+
+ this._logger?.LogDebug("Invoked {FunctionName}. Result: {Result}", targetFunction.Name, result.Result);
+
+ return result.Result;
+ }
+ catch (Exception e) when (!e.IsCriticalException())
+ {
+ this._logger?.LogError(e, "Something went wrong in system step: {0}.{1}. Error: {2}", targetFunction.SkillName, targetFunction.Name, e.Message);
+ return $"Something went wrong in system step: {targetFunction.SkillName}.{targetFunction.Name}. Error: {e.Message} {e.InnerException.Message}";
+ }
+ }
+
+ private SKContext CreateActionContext(Dictionary actionVariables)
+ {
+ var actionContext = this._kernel.CreateNewContext();
+ if (actionVariables != null)
+ {
+ foreach (var kvp in actionVariables)
+ {
+ actionContext.Variables.Set(kvp.Key, kvp.Value);
+ }
+ }
+
+ return actionContext;
+ }
+
+ private IEnumerable GetAvailableFunctions()
+ {
+ FunctionsView functionsView = this._context.Skills!.GetFunctionsView();
+
+ var excludedSkills = this.Config.ExcludedSkills ?? new();
+ var excludedFunctions = this.Config.ExcludedFunctions ?? new();
+
+ var availableFunctions =
+ functionsView.NativeFunctions
+ .Concat(functionsView.SemanticFunctions)
+ .SelectMany(x => x.Value)
+ .Where(s => !excludedSkills.Contains(s.SkillName) && !excludedFunctions.Contains(s.Name))
+ .OrderBy(x => x.SkillName)
+ .ThenBy(x => x.Name);
+ return availableFunctions;
+ }
+
+ private string GetFunctionDescriptions()
+ {
+ var availableFunctions = this.GetAvailableFunctions();
+
+ string functionDescriptions = string.Join("\n", availableFunctions.Select(x => ToManualString(x)));
+ return functionDescriptions;
+ }
+
+ private ISKFunction ImportSemanticFunction(IKernel kernel, string functionName, string promptTemplate, PromptTemplateConfig config)
+ {
+ var template = new PromptTemplate(promptTemplate, config, kernel.PromptTemplateEngine);
+ var functionConfig = new SemanticFunctionConfig(config, template);
+
+ return kernel.RegisterSemanticFunction(RestrictedSkillName, functionName, functionConfig);
+ }
+
+ private static string ToManualString(FunctionView function)
+ {
+ var inputs = string.Join("\n", function.Parameters.Select(parameter =>
+ {
+ var defaultValueString = string.IsNullOrEmpty(parameter.DefaultValue) ? string.Empty : $"(default='{parameter.DefaultValue}')";
+ return $" - {parameter.Name}: {parameter.Description} {defaultValueString}";
+ }));
+
+ var functionDescription = function.Description.Trim();
+
+ if (string.IsNullOrEmpty(inputs))
+ {
+ return $"{ToFullyQualifiedName(function)}: {functionDescription}\n";
+ }
+
+ return $"{ToFullyQualifiedName(function)}: {functionDescription}\n{inputs}\n";
+ }
+
+ private static string ToFullyQualifiedName(FunctionView function)
+ {
+ return $"{function.SkillName}.{function.Name}";
+ }
+
+ ///
+ /// The configuration for the StepwisePlanner
+ ///
+ private StepwisePlannerConfig Config { get; }
+
+ // Context used to access the list of functions in the kernel
+ private readonly SKContext _context;
+ private readonly IKernel _kernel;
+ private readonly ILogger _logger;
+
+ ///
+ /// Planner native functions
+ ///
+ private IDictionary _nativeFunctions = new Dictionary();
+
+ ///
+ /// System step function for Plan execution
+ ///
+ private ISKFunction _systemStepFunction;
+
+ ///
+ /// The name to use when creating semantic functions that are restricted from plan creation
+ ///
+ private const string RestrictedSkillName = "StepwisePlanner_Excluded";
+
+ ///
+ /// The Action tag
+ ///
+ private const string Action = "[ACTION]";
+
+ ///
+ /// The Thought tag
+ ///
+ private const string Thought = "[THOUGHT]";
+
+ ///
+ /// The Observation tag
+ ///
+ private const string Observation = "[OBSERVATION]";
+
+ ///
+ /// The prefix used for the scratch pad
+ ///
+ private const string ScratchPadPrefix = "This was my previous work (but they haven't seen any of it! They only see what I return as final answer):";
+
+ ///
+ /// The regex for parsing the action response
+ ///
+ private static readonly Regex s_actionRegex = new(@"\[ACTION\][^{}]*({(?:[^{}]*{[^{}]*})*[^{}]*})", RegexOptions.Singleline);
+
+ ///
+ /// The regex for parsing the thought response
+ ///
+ private static readonly Regex s_thoughtRegex = new(@"(\[THOUGHT\])?(?.+?)(?=\[ACTION\]|$)", RegexOptions.Singleline);
+
+ ///
+ /// The regex for parsing the final answer response
+ ///
+ private static readonly Regex s_finalAnswerRegex = new(@"\[FINAL ANSWER\](?.+)", RegexOptions.Singleline);
+}
diff --git a/dotnet/src/Extensions/Planning.StepwisePlanner/StepwisePlannerConfig.cs b/dotnet/src/Extensions/Planning.StepwisePlanner/StepwisePlannerConfig.cs
new file mode 100644
index 000000000000..c50a1dbec2e1
--- /dev/null
+++ b/dotnet/src/Extensions/Planning.StepwisePlanner/StepwisePlannerConfig.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+
+namespace Microsoft.SemanticKernel.Planning.Stepwise;
+
+///
+/// Configuration for Stepwise planner instances.
+///
+public sealed class StepwisePlannerConfig
+{
+ ///
+ /// The minimum relevancy score for a function to be considered
+ ///
+ ///
+ /// Depending on the embeddings engine used, the user ask, the step goal
+ /// and the functions available, this value may need to be adjusted.
+ /// For default, this is set to null to exhibit previous behavior.
+ ///
+ public double? RelevancyThreshold { get; set; }
+
+ ///
+ /// The maximum number of relevant functions to include in the plan.
+ ///
+ ///
+ /// Limits the number of relevant functions as result of semantic
+ /// search included in the plan creation request.
+ /// will be included
+ /// in the plan regardless of this limit.
+ ///
+ public int MaxRelevantFunctions { get; set; } = 100;
+
+ ///
+ /// A list of skills to exclude from the plan creation request.
+ ///
+ public HashSet ExcludedSkills { get; } = new();
+
+ ///
+ /// A list of functions to exclude from the plan creation request.
+ ///
+ public HashSet ExcludedFunctions { get; } = new();
+
+ ///
+ /// A list of functions to include in the plan creation request.
+ ///
+ public HashSet IncludedFunctions { get; } = new();
+
+ ///
+ /// The maximum number of tokens to allow in a plan.
+ ///
+ public int MaxTokens { get; set; } = 1024;
+
+ ///
+ /// The maximum number of iterations to allow in a plan.
+ ///
+ public int MaxIterations { get; set; } = 100;
+
+ ///
+ /// The minimum time to wait between iterations in milliseconds.
+ ///
+ public int MinIterationTimeMs { get; set; } = 0;
+}
diff --git a/dotnet/src/Extensions/Planning.StepwisePlanner/SystemStep.cs b/dotnet/src/Extensions/Planning.StepwisePlanner/SystemStep.cs
new file mode 100644
index 000000000000..3fc8ed2dffd3
--- /dev/null
+++ b/dotnet/src/Extensions/Planning.StepwisePlanner/SystemStep.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel.Planning.Stepwise;
+
+///
+/// A step in a Stepwise plan.
+///
+public class SystemStep
+{
+ ///
+ /// Gets or sets the step number.
+ ///
+ [JsonPropertyName("thought")]
+ public string? Thought { get; set; }
+
+ ///
+ /// Gets or sets the action of the step
+ ///
+ [JsonPropertyName("action")]
+ public string? Action { get; set; }
+
+ ///
+ /// Gets or sets the variables for the action
+ ///
+ [JsonPropertyName("action_variables")]
+ public Dictionary? ActionVariables { get; set; }
+
+ ///
+ /// Gets or sets the output of the action
+ ///
+ [JsonPropertyName("observation")]
+ public string? Observation { get; set; }
+
+ ///
+ /// Gets or sets the output of the system
+ ///
+ [JsonPropertyName("final_answer")]
+ public string? FinalAnswer { get; set; }
+
+ ///
+ /// The raw response from the action
+ ///
+ [JsonPropertyName("original_response")]
+ public string? OriginalResponse { get; set; }
+}
diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj
index 8d0218baac39..56a65c361226 100644
--- a/dotnet/src/IntegrationTests/IntegrationTests.csproj
+++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj
@@ -36,7 +36,9 @@
+
+
diff --git a/dotnet/src/IntegrationTests/Planning/StepwisePlanner/StepwisePlannerTests.cs b/dotnet/src/IntegrationTests/Planning/StepwisePlanner/StepwisePlannerTests.cs
new file mode 100644
index 000000000000..a47c026a1086
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Planning/StepwisePlanner/StepwisePlannerTests.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Memory;
+using Microsoft.SemanticKernel.Planning.Stepwise;
+using Microsoft.SemanticKernel.Skills.Core;
+using Microsoft.SemanticKernel.Skills.Web;
+using Microsoft.SemanticKernel.Skills.Web.Bing;
+using SemanticKernel.IntegrationTests.Fakes;
+using SemanticKernel.IntegrationTests.TestSettings;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SemanticKernel.IntegrationTests.Planning.StepwisePlanner;
+
+public sealed class StepwisePlannerTests : IDisposable
+{
+ private readonly string _bingApiKey;
+
+ public StepwisePlannerTests(ITestOutputHelper output)
+ {
+ this._logger = NullLogger.Instance; //new XunitLogger