Skip to content

Commit 49e2010

Browse files
Add StepwisePlanner extension for MRKL style planning (#1468)
### Motivation and Context This pull request adds a new extension for semantic planning using a stepwise approach. The StepwisePlanner extension allows users to create and execute plans that consist of a sequence of semantic and native functions, each with a goal and a set of inputs and outputs. The extension uses a semantic search engine to find relevant functions for each step, and a plan creation service to generate a plan that satisfies the user's ask. The extension also provides a system step function that executes the plan and returns the final answer and intermediate observations. The extension can be configured with various parameters, such as the relevancy threshold, the maximum number of relevant functions, the excluded and included functions and skills, and the maximum number of tokens, iterations, and time for the plan. Regarding #1472 ### Description - Add StepwisePlanner.cs, which registers the planner native functions and the system step function - Add StepwisePlannerConfig.cs, which defines the configuration options for the StepwisePlanner extension - Add SystemStep.cs, which represents a step in a Stepwise plan, with properties for the thought, action, action variables, observation, final answer, and original response - Add helper methods for formatting and validating function views, generating plan requests, and invoking the plan. - Add logging and error handling for the planner extension - Add unit tests for the planner extension and the native functions ### Related - Majority of work initially started from @kaza in #992 ### Changes in other PRs to merge separately - #1464 - #1465 - #1466 ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows SK Contribution Guidelines (https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) - [x] The code follows the .NET coding conventions (https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions) verified with `dotnet format` - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Almir Kazazic <kaza@users.noreply.github.com> --------- Co-authored-by: Lee Miller <lemillermicrosoft@users.noreply.github.com>
1 parent 85d420f commit 49e2010

File tree

16 files changed

+1176
-11
lines changed

16 files changed

+1176
-11
lines changed

dotnet/SK-dotnet.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Skills.Core", "src\Skills\S
150150
EndProject
151151
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCalcSkills", "samples\NCalcSkills\NCalcSkills.csproj", "{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}"
152152
EndProject
153+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Planning.StepwisePlanner", "src\Extensions\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj", "{4762BCAF-E1C5-4714-B88D-E50FA333C50E}"
154+
EndProject
153155
Global
154156
GlobalSection(SolutionConfigurationPlatforms) = preSolution
155157
Debug|Any CPU = Debug|Any CPU
@@ -375,6 +377,12 @@ Global
375377
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Publish|Any CPU.Build.0 = Debug|Any CPU
376378
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
377379
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Release|Any CPU.Build.0 = Release|Any CPU
380+
{4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
381+
{4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Debug|Any CPU.Build.0 = Debug|Any CPU
382+
{4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
383+
{4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Publish|Any CPU.Build.0 = Publish|Any CPU
384+
{4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Release|Any CPU.ActiveCfg = Release|Any CPU
385+
{4762BCAF-E1C5-4714-B88D-E50FA333C50E}.Release|Any CPU.Build.0 = Release|Any CPU
378386
EndGlobalSection
379387
GlobalSection(SolutionProperties) = preSolution
380388
HideSolutionNode = FALSE
@@ -429,6 +437,7 @@ Global
429437
{185E0CE8-C2DA-4E4C-A491-E8EB40316315} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
430438
{0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
431439
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA} = {FA3720F1-C99A-49B2-9577-A940257098BF}
440+
{4762BCAF-E1C5-4714-B88D-E50FA333C50E} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
432441
EndGlobalSection
433442
GlobalSection(ExtensibilityGlobals) = postSolution
434443
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Diagnostics;
5+
using System.Threading.Tasks;
6+
using Microsoft.SemanticKernel;
7+
using Microsoft.SemanticKernel.Planning;
8+
using Microsoft.SemanticKernel.Reliability;
9+
using Microsoft.SemanticKernel.Skills.Core;
10+
using Microsoft.SemanticKernel.Skills.Web;
11+
using Microsoft.SemanticKernel.Skills.Web.Bing;
12+
using NCalcSkills;
13+
using RepoUtils;
14+
15+
/**
16+
* This example shows how to use Stepwise Planner to create a plan for a given goal.
17+
*/
18+
19+
// ReSharper disable once InconsistentNaming
20+
public static class Example51_StepwisePlanner
21+
{
22+
public static async Task RunAsync()
23+
{
24+
string[] questions = new string[]
25+
{
26+
"Who is the current president of the United States? What is his current age divided by 2",
27+
// "Who is Leo DiCaprio's girlfriend? What is her current age raised to the (his current age)/100 power?",
28+
// "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?",
29+
// "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?"
30+
};
31+
32+
foreach (var question in questions)
33+
{
34+
await RunTextCompletion(question);
35+
await RunChatCompletion(question);
36+
}
37+
}
38+
39+
public static async Task RunTextCompletion(string question)
40+
{
41+
Console.WriteLine("RunTextCompletion");
42+
var kernel = GetKernel();
43+
await RunWithQuestion(kernel, question);
44+
}
45+
46+
public static async Task RunChatCompletion(string question)
47+
{
48+
Console.WriteLine("RunChatCompletion");
49+
var kernel = GetKernel(true);
50+
await RunWithQuestion(kernel, question);
51+
}
52+
53+
public static async Task RunWithQuestion(IKernel kernel, string question)
54+
{
55+
using var bingConnector = new BingConnector(Env.Var("BING_API_KEY"));
56+
var webSearchEngineSkill = new WebSearchEngineSkill(bingConnector);
57+
58+
kernel.ImportSkill(webSearchEngineSkill, "WebSearch");
59+
kernel.ImportSkill(new LanguageCalculatorSkill(kernel), "advancedCalculator");
60+
// kernel.ImportSkill(new SimpleCalculatorSkill(kernel), "basicCalculator");
61+
kernel.ImportSkill(new TimeSkill(), "time");
62+
63+
Console.WriteLine("*****************************************************");
64+
Stopwatch sw = new();
65+
Console.WriteLine("Question: " + question);
66+
67+
var config = new Microsoft.SemanticKernel.Planning.Stepwise.StepwisePlannerConfig();
68+
config.ExcludedFunctions.Add("TranslateMathProblem");
69+
config.MinIterationTimeMs = 1500;
70+
config.MaxTokens = 4000;
71+
72+
StepwisePlanner planner = new(kernel, config);
73+
sw.Start();
74+
var plan = planner.CreatePlan(question);
75+
76+
var result = await plan.InvokeAsync(kernel.CreateNewContext());
77+
Console.WriteLine("Result: " + result);
78+
if (result.Variables.TryGetValue("stepCount", out string? stepCount))
79+
{
80+
Console.WriteLine("Steps Taken: " + stepCount);
81+
}
82+
83+
if (result.Variables.TryGetValue("skillCount", out string? skillCount))
84+
{
85+
Console.WriteLine("Skills Used: " + skillCount);
86+
}
87+
88+
Console.WriteLine("Time Taken: " + sw.Elapsed);
89+
Console.WriteLine("*****************************************************");
90+
}
91+
92+
private static IKernel GetKernel(bool useChat = false)
93+
{
94+
var builder = new KernelBuilder();
95+
if (useChat)
96+
{
97+
builder.WithAzureChatCompletionService(
98+
Env.Var("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"),
99+
Env.Var("AZURE_OPENAI_ENDPOINT"),
100+
Env.Var("AZURE_OPENAI_KEY"),
101+
alsoAsTextCompletion: true,
102+
setAsDefault: true);
103+
}
104+
else
105+
{
106+
builder.WithAzureTextCompletionService(
107+
Env.Var("AZURE_OPENAI_DEPLOYMENT_NAME"),
108+
Env.Var("AZURE_OPENAI_ENDPOINT"),
109+
Env.Var("AZURE_OPENAI_KEY"));
110+
}
111+
112+
var kernel = builder
113+
.WithLogger(ConsoleLogger.Log)
114+
.Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig
115+
{
116+
MaxRetryCount = 3,
117+
UseExponentialBackoff = true,
118+
MinRetryDelay = TimeSpan.FromSeconds(3),
119+
}))
120+
.Build();
121+
122+
return kernel;
123+
}
124+
}
125+
126+
// RunTextCompletion
127+
// *****************************************************
128+
// Question: Who is the current president of the United States? What is his current age divided by 2
129+
// Result: The current president of the United States is Joe Biden. His current age divided by 2 is 40.
130+
// Steps Taken: 10
131+
// Skills Used: 4 (WebSearch.Search(2), time.Date(1), advancedCalculator.Calculator(1))
132+
// Time Taken: 00:00:53.6331324
133+
// *****************************************************
134+
// RunChatCompletion
135+
// *****************************************************
136+
// Question: Who is the current president of the United States? What is his current age divided by 2
137+
// Result: The current president of the United States is Joe Biden. His current age divided by 2 is 40.5.
138+
// Steps Taken: 9
139+
// Skills Used: 7 (WebSearch.Search(4), time.Year(1), time.Date(1), advancedCalculator.Calculator(1))
140+
// Time Taken: 00:01:13.3766860
141+
// *****************************************************
142+
// RunTextCompletion
143+
// *****************************************************
144+
// Question: Who is Leo DiCaprio's girlfriend? What is her current age raised to the (his current age)/100 power?
145+
// Result: Leo DiCaprio's girlfriend is Camila Morrone. Her current age raised to the (his current age)/100 power is 4.935565735151678.
146+
// Steps Taken: 6
147+
// Skills Used: 5 (WebSearch.Search(3), time.Year(1), advancedCalculator.Calculator(1))
148+
// Time Taken: 00:00:37.8941510
149+
// *****************************************************
150+
// RunChatCompletion
151+
// *****************************************************
152+
// Question: Who is Leo DiCaprio's girlfriend? What is her current age raised to the (his current age)/100 power?
153+
// Result: Leo DiCaprio's girlfriend is Camila Morrone. Her current age raised to the power of (his current age)/100 is approximately 4.94.
154+
// Steps Taken: 9
155+
// Skills Used: 5 (WebSearch.Search(3), time.Year(1), advancedCalculator.Calculator(1))
156+
// Time Taken: 00:01:17.6742136
157+
// *****************************************************
158+
// RunTextCompletion
159+
// *****************************************************
160+
// 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?
161+
// 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.
162+
// Steps Taken: 7
163+
// Skills Used: 4 (WebSearch.Search(3), advancedCalculator.Calculator(1))
164+
// Time Taken: 00:00:41.6837628
165+
// *****************************************************
166+
// RunChatCompletion
167+
// *****************************************************
168+
// 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?
169+
// 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.
170+
// Steps Taken: 14
171+
// Skills Used: 12 (WebSearch.Search(8), time.Year(1), advancedCalculator.Calculator(3))
172+
// Time Taken: 00:02:06.6682909
173+
// *****************************************************
174+
// RunTextCompletion
175+
// *****************************************************
176+
// 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?
177+
// 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.
178+
// Steps Taken: 16
179+
// Skills Used: 2 (time.DayOfYear(1), time.Date(1))
180+
// Time Taken: 00:01:29.9931039
181+
// *****************************************************
182+
// RunChatCompletion
183+
// *****************************************************
184+
// 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?
185+
// 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.
186+
// Steps Taken: 11
187+
// Skills Used: 9 (time.Now(1), time.DayOfYear(1), time.DaysBetween(1), time.MonthNumber(1), time.Day(1), advancedCalculator.Calculator(4))
188+
// Time Taken: 00:01:41.5585861
189+
// *****************************************************

dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
44
</PropertyGroup>
@@ -35,13 +35,15 @@
3535
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Redis\Connectors.Memory.Redis.csproj" />
3636
<ProjectReference Include="..\..\src\Extensions\Planning.ActionPlanner\Planning.ActionPlanner.csproj" />
3737
<ProjectReference Include="..\..\src\Extensions\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
38+
<ProjectReference Include="..\..\src\Extensions\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj" />
3839
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Pinecone\Connectors.Memory.Pinecone.csproj" />
3940
<ProjectReference Include="..\..\src\Skills\Skills.Core\Skills.Core.csproj" />
4041
<ProjectReference Include="..\..\src\Skills\Skills.OpenAPI\Skills.OpenAPI.csproj" />
4142
<ProjectReference Include="..\..\src\Skills\Skills.Grpc\Skills.Grpc.csproj" />
4243
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Qdrant\Connectors.Memory.Qdrant.csproj" />
4344
<ProjectReference Include="..\..\src\Skills\Skills.Web\Skills.Web.csproj" />
4445
<ProjectReference Include="..\..\src\SemanticKernel\SemanticKernel.csproj" />
46+
<ProjectReference Include="..\NCalcSkills\NCalcSkills.csproj" />
4547
</ItemGroup>
4648
<ItemGroup>
4749
<EmbeddedResource Include="Resources\30-user-prompt.txt" />

dotnet/samples/KernelSyntaxExamples/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,8 @@ public static async Task Main()
157157

158158
await Example50_Chroma.RunAsync();
159159
Console.WriteLine("== DONE ==");
160+
161+
await Example51_StepwisePlanner.RunAsync();
162+
Console.WriteLine("== DONE ==");
160163
}
161164
}
Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2-
<PropertyGroup>
3-
<RepoRoot>$([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)'))))</RepoRoot>
4-
</PropertyGroup>
5-
62
<PropertyGroup>
73
<TargetFramework>netstandard2.0</TargetFramework>
84
<LangVersion>10</LangVersion>
@@ -12,13 +8,7 @@
128
<ProjectReference Include="..\..\..\dotnet\src\SemanticKernel\SemanticKernel.csproj" />
139
</ItemGroup>
1410

15-
<PropertyGroup>
16-
<RepoRoot>$([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)'))))</RepoRoot>
17-
</PropertyGroup>
18-
19-
2011
<ItemGroup>
2112
<PackageReference Include="CoreCLR-NCalc"/>
2213
</ItemGroup>
23-
2414
</Project>

dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<ItemGroup>
3131
<ProjectReference Include="..\Planning.ActionPlanner\Planning.ActionPlanner.csproj" />
3232
<ProjectReference Include="..\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
33+
<ProjectReference Include="..\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj" />
3334
</ItemGroup>
3435

3536
</Project>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Collections.Generic;
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.SemanticKernel;
6+
using Microsoft.SemanticKernel.SkillDefinition;
7+
using Moq;
8+
using Xunit;
9+
10+
namespace SemanticKernel.Extensions.UnitTests.Planning.StepwisePlanner;
11+
12+
public sealed class ParseResultTests
13+
{
14+
[Theory]
15+
[InlineData("[FINAL ANSWER] 42", "42")]
16+
[InlineData("[FINAL ANSWER]42", "42")]
17+
[InlineData("I think I have everything I need.\n[FINAL ANSWER] 42", "42")]
18+
[InlineData("I think I have everything I need.\n[FINAL ANSWER] 42\n", "42")]
19+
[InlineData("I think I have everything I need.\n[FINAL ANSWER] 42\n\n", "42")]
20+
[InlineData("I think I have everything I need.\n[FINAL ANSWER]42\n\n\n", "42")]
21+
[InlineData("I think I have everything I need.\n[FINAL ANSWER]\n 42\n\n\n", "42")]
22+
public void WhenInputIsFinalAnswerReturnsFinalAnswer(string input, string expected)
23+
{
24+
// Arrange
25+
var kernel = new Mock<IKernel>();
26+
kernel.Setup(x => x.Log).Returns(new Mock<ILogger>().Object);
27+
28+
var planner = new Microsoft.SemanticKernel.Planning.StepwisePlanner(kernel.Object);
29+
30+
// Act
31+
var result = planner.ParseResult(input);
32+
33+
// Assert
34+
Assert.Equal(expected, result.FinalAnswer);
35+
}
36+
37+
[Theory]
38+
[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")]
39+
[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")]
40+
[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",
41+
"Camila Morrone age", "count", "1")]
42+
public void ParseActionReturnsAction(string input, string expectedAction, params string[] expectedVariables)
43+
{
44+
Dictionary<string, string>? expectedDictionary = null;
45+
for (int i = 0; i < expectedVariables.Length; i += 2)
46+
{
47+
expectedDictionary ??= new Dictionary<string, string>();
48+
expectedDictionary.Add(expectedVariables[i], expectedVariables[i + 1]);
49+
}
50+
51+
// Arrange
52+
var kernel = new Mock<IKernel>();
53+
kernel.Setup(x => x.Log).Returns(new Mock<ILogger>().Object);
54+
55+
var planner = new Microsoft.SemanticKernel.Planning.StepwisePlanner(kernel.Object);
56+
57+
// Act
58+
var result = planner.ParseResult(input);
59+
60+
// Assert
61+
Assert.Equal(expectedAction, result.Action);
62+
Assert.Equal(expectedDictionary, result.ActionVariables);
63+
}
64+
65+
// Method to create Mock<ISKFunction> objects
66+
private static Mock<ISKFunction> CreateMockFunction(FunctionView functionView)
67+
{
68+
var mockFunction = new Mock<ISKFunction>();
69+
mockFunction.Setup(x => x.Describe()).Returns(functionView);
70+
mockFunction.Setup(x => x.Name).Returns(functionView.Name);
71+
mockFunction.Setup(x => x.SkillName).Returns(functionView.SkillName);
72+
return mockFunction;
73+
}
74+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.IO;
4+
using System.Reflection;
5+
6+
namespace Microsoft.SemanticKernel.Planning.Stepwise;
7+
8+
internal static class EmbeddedResource
9+
{
10+
private static readonly string? s_namespace = typeof(EmbeddedResource).Namespace;
11+
12+
internal static string Read(string name)
13+
{
14+
var assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
15+
if (assembly == null) { throw new PlanningException(PlanningException.ErrorCodes.InvalidConfiguration, $"[{s_namespace}] {name} assembly not found"); }
16+
17+
using Stream? resource = assembly.GetManifestResourceStream($"{s_namespace}." + name);
18+
if (resource == null) { throw new PlanningException(PlanningException.ErrorCodes.InvalidConfiguration, $"[{s_namespace}] {name} resource not found"); }
19+
20+
using var reader = new StreamReader(resource);
21+
return reader.ReadToEnd();
22+
}
23+
}

0 commit comments

Comments
 (0)