diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 02ec385253..815042b6be 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -137,6 +137,28 @@
"cwd": "${workspaceFolder}/dotnet/src/SemanticKernel.UnitTests/"
}
},
+ {
+ "label": "Test - Extensions (Code Coverage)",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "test",
+ "--collect",
+ "XPlat Code Coverage;Format=lcov",
+ "--filter",
+ "${input:filter}",
+ "Extensions.UnitTests.csproj"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": "test",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared"
+ },
+ "options": {
+ "cwd": "${workspaceFolder}/dotnet/src/Extensions/Extensions.UnitTests/"
+ }
+ },
{
"label": "Test - Semantic-Kernel Integration (Code Coverage)",
"command": "dotnet",
diff --git a/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj b/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj
index c40d26b48a..a3eb070896 100644
--- a/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj
+++ b/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj
@@ -18,6 +18,10 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
diff --git a/dotnet/src/IntegrationTests/Planning/PlanTests.cs b/dotnet/src/IntegrationTests/Planning/PlanTests.cs
index 96adc68f7b..62a4460cc6 100644
--- a/dotnet/src/IntegrationTests/Planning/PlanTests.cs
+++ b/dotnet/src/IntegrationTests/Planning/PlanTests.cs
@@ -157,6 +157,40 @@ public sealed class PlanTests : IDisposable
Assert.Equal($"Sent email to: {email}. Body: {expectedBody}".Trim(), plan.State.ToString());
}
+ [Theory]
+ [InlineData(null, "Write a poem or joke and send it in an e-mail to Kai.", null)]
+ [InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")]
+ [InlineData("Hello World!", "Write a poem or joke and send it in an e-mail to Kai.", "some_email@email.com")]
+ public async Task CanExecuteRunPlanSimpleManualStateNoVariableAsync(string input, string goal, string email)
+ {
+ // Arrange
+ IKernel target = this.InitializeKernel();
+ var emailSkill = target.ImportSkill(new EmailSkillFake());
+
+ // Create the input mapping from parent (plan) plan state to child plan (sendEmailPlan) state.
+ var cv = new ContextVariables();
+ cv.Set("email_address", string.Empty);
+ var sendEmailPlan = new Plan(emailSkill["SendEmailAsync"])
+ {
+ NamedParameters = cv,
+ };
+
+ var plan = new Plan(goal);
+ plan.AddSteps(sendEmailPlan);
+ plan.State.Set("email_address", email); // manually prepare the state
+
+ // Act
+ var result = await target.StepAsync(input, plan);
+
+ // Assert
+ var expectedBody = string.IsNullOrEmpty(input) ? goal : input;
+ Assert.Single(result.Steps);
+ Assert.Equal(1, result.NextStepIndex);
+ Assert.False(result.HasNextStep);
+ Assert.Equal(goal, plan.Description);
+ Assert.Equal($"Sent email to: {email}. Body: {expectedBody}".Trim(), plan.State.ToString());
+ }
+
[Theory]
[InlineData(null, "Write a poem or joke and send it in an e-mail to Kai.", null)]
[InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")]
@@ -314,6 +348,63 @@ public sealed class PlanTests : IDisposable
Assert.True(expectedBody.Length < result.Result.Length);
}
+ [Theory]
+ [InlineData("Summarize an input, translate to french, and e-mail to Kai", "This is a story about a dog.", "French", "Kai", "Kai@example.com")]
+ public async Task CanExecuteRunSequentialOnDeserializedPlanAsync(string goal, string inputToSummarize, string inputLanguage, string inputName,
+ string expectedEmail)
+ {
+ // Arrange
+ IKernel target = this.InitializeKernel();
+ var summarizeSkill = TestHelpers.GetSkills(target, "SummarizeSkill");
+ var writerSkill = TestHelpers.GetSkills(target, "WriterSkill");
+ var emailSkill = target.ImportSkill(new EmailSkillFake());
+
+ var expectedBody = $"Sent email to: {expectedEmail}. Body:".Trim();
+
+ var summarizePlan = new Plan(summarizeSkill["Summarize"]);
+
+ var cv = new ContextVariables();
+ cv.Set("language", inputLanguage);
+ var outputs = new ContextVariables();
+ outputs.Set("TRANSLATED_SUMMARY", string.Empty);
+
+ var translatePlan = new Plan(writerSkill["Translate"])
+ {
+ NamedParameters = cv,
+ NamedOutputs = outputs,
+ };
+
+ cv = new ContextVariables();
+ cv.Update(inputName);
+ outputs = new ContextVariables();
+ outputs.Set("TheEmailFromState", string.Empty);
+ var getEmailPlan = new Plan(emailSkill["GetEmailAddressAsync"])
+ {
+ NamedParameters = cv,
+ NamedOutputs = outputs,
+ };
+
+ cv = new ContextVariables();
+ cv.Set("email_address", "$TheEmailFromState");
+ cv.Set("input", "$TRANSLATED_SUMMARY");
+ var sendEmailPlan = new Plan(emailSkill["SendEmailAsync"])
+ {
+ NamedParameters = cv
+ };
+
+ var plan = new Plan(goal);
+ plan.AddSteps(summarizePlan, translatePlan, getEmailPlan, sendEmailPlan);
+
+ // Act
+ var serializedPlan = plan.ToJson();
+ var deserializedPlan = Plan.FromJson(serializedPlan, target.CreateNewContext());
+ var result = await target.RunAsync(inputToSummarize, deserializedPlan);
+
+ // Assert
+ Assert.Contains(expectedBody, result.Result, StringComparison.OrdinalIgnoreCase);
+ Assert.True(expectedBody.Length < result.Result.Length);
+ }
+
[Theory]
[InlineData("Summarize an input, translate to french, and e-mail to Kai", "This is a story about a dog.", "French", "kai@email.com")]
public async Task CanExecuteRunSequentialFunctionsAsync(string goal, string inputToSummarize, string inputLanguage, string expectedEmail)
diff --git a/dotnet/src/SemanticKernel.UnitTests/Orchestration/ContextVariablesConverterTests.cs b/dotnet/src/SemanticKernel.UnitTests/Orchestration/ContextVariablesConverterTests.cs
new file mode 100644
index 0000000000..05f1e7ec8c
--- /dev/null
+++ b/dotnet/src/SemanticKernel.UnitTests/Orchestration/ContextVariablesConverterTests.cs
@@ -0,0 +1,186 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Orchestration;
+using Xunit;
+
+namespace SemanticKernel.UnitTests.Orchestration;
+
+///
+/// Unit tests of .
+///
+public class ContextVariablesConverterTests
+{
+ [Fact]
+ public void ReadFromJsonSucceeds()
+ {
+ // Arrange
+ string json = /*lang=json,strict*/ @"[{""Key"":""a"", ""Value"":""b""}]";
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+
+ // Act
+ var result = JsonSerializer.Deserialize(json, options);
+
+ // Assert
+ Assert.Equal("b", result!["a"]);
+ Assert.Equal(string.Empty, result!["INPUT"]);
+ }
+
+ [Fact]
+ public void ReadFromJsonWrongTypeThrows()
+ {
+ // Arrange
+ string json = /*lang=json,strict*/ @"[{""Key"":""a"", ""Value"":""b""}]";
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+
+ // Act and Assert
+ Assert.Throws(() => JsonSerializer.Deserialize>(json, options));
+ }
+
+ [Fact]
+ public void ReadFromJsonSucceedsWithInput()
+ {
+ // Arrange
+ string json = /*lang=json,strict*/ @"[{""Key"":""INPUT"", ""Value"":""c""}, {""Key"":""a"", ""Value"":""b""}]";
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+
+ // Act
+ var result = JsonSerializer.Deserialize(json, options);
+
+ // Assert
+ Assert.Equal("b", result!["a"]);
+ Assert.Equal("c", result!["INPUT"]);
+ }
+
+ // input value
+ // params key/value
+ [Theory]
+ [InlineData(null, new[] { "a", "b" }, new[]
+ {
+ /*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""""}", /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}"
+ })]
+ [InlineData("", new[] { "a", "b" }, new[]
+ {
+ /*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""""}", /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}"
+ })]
+ [InlineData("c", new[] { "a", "b" }, new[]
+ {
+ /*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""c""}", /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}"
+ })]
+ [InlineData("c", new[] { "a", "b", "d", "e" }, new[]
+ {
+ /*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""c""}", /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}", /*lang=json,strict*/
+ @"{""Key"":""d"",""Value"":""e""}"
+ })]
+ public void WriteToJsonSuceeds(string inputValue, IList contextToSet, IList expectedJson)
+ {
+ // Arrange
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+ var context = new ContextVariables();
+ if (inputValue != null)
+ {
+ context.Update(inputValue);
+ }
+
+ for (int i = 0; i < contextToSet.Count; i += 2)
+ {
+ context.Set(contextToSet[i], contextToSet[i + 1]);
+ }
+
+ // Act
+ string result = JsonSerializer.Serialize(context, options);
+
+ // Assert
+ foreach (string key in expectedJson)
+ {
+ Assert.Contains(key, result, StringComparison.Ordinal);
+ }
+ }
+
+ [Fact]
+ public void WriteToJsonSucceedsAfterClearing()
+ {
+ // Arrange
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+ var context = new ContextVariables();
+ context.Set("a", "b");
+ context.Set("INPUT", "c");
+ context.Set("d", "e");
+ context.Set("f", "ThingToBeCleared");
+ context.Set("f", null);
+ context.Set("g", string.Empty);
+
+ // Act
+ string result = JsonSerializer.Serialize(context, options);
+
+ // Assert
+ Assert.Contains( /*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""c""}", result, StringComparison.Ordinal);
+ Assert.Contains( /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}", result, StringComparison.Ordinal);
+ Assert.Contains( /*lang=json,strict*/ @"{""Key"":""d"",""Value"":""e""}", result, StringComparison.Ordinal);
+ Assert.DoesNotContain(@"""Key"":""f""", result, StringComparison.Ordinal);
+ Assert.Contains( /*lang=json,strict*/ @"{""Key"":""g"",""Value"":""""}", result, StringComparison.Ordinal);
+ }
+
+ // Error Tests
+ [Fact]
+ public void ReadFromJsonReturnsNullWithNull()
+ {
+ // Arrange
+ string json = /*lang=json,strict*/ @"null";
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+
+ // Act
+ var result = JsonSerializer.Deserialize(json, options);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ReadFromJsonReturnsDefaultWithEmpty()
+ {
+ // Arrange
+ string json = /*lang=json,strict*/ @"[]";
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+
+ // Act
+ var result = JsonSerializer.Deserialize(json, options);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(string.Empty, result!["INPUT"]);
+ }
+
+ [Fact]
+ public void ReadFromJsonThrowsWithInvalidJson()
+ {
+ // Arrange
+ string json = /*lang=json,strict*/ @"[{""Key"":""a"", ""Value"":""b""";
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+
+ // Act & Assert
+ Assert.Throws(() => JsonSerializer.Deserialize(json, options));
+ }
+
+ [Fact]
+ public void ReadFromJsonThrowsWithInvalidJson2()
+ {
+ // Arrange
+ string json = /*lang=json,strict*/ @"[{""Keys"":""a"", ""Value"":""b""}]";
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new ContextVariablesConverter());
+
+ // Act & Assert
+ Assert.Throws(() => JsonSerializer.Deserialize(json, options));
+ }
+}
diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs
new file mode 100644
index 0000000000..776cd0cc54
--- /dev/null
+++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs
@@ -0,0 +1,538 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.AI.TextCompletion;
+using Microsoft.SemanticKernel.Memory;
+using Microsoft.SemanticKernel.Orchestration;
+using Microsoft.SemanticKernel.Planning;
+using Microsoft.SemanticKernel.SkillDefinition;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.UnitTests.Planning;
+
+public sealed class PlanSerializationTests
+{
+ [Fact]
+ public void CanSerializePlan()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var expectedSteps = "\"steps\":[]";
+ var plan = new Plan(goal);
+
+ // Act
+ var serializedPlan = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan);
+ Assert.NotEmpty(serializedPlan);
+ Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CanSerializePlanWithGoalAndSteps()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var expectedSteps = "\"steps\":[{";
+ var plan = new Plan(goal, new Mock().Object, new Mock().Object);
+
+ // Act
+ var serializedPlan = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan);
+ Assert.NotEmpty(serializedPlan);
+ Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CanSerializePlanWithGoalAndSubPlans()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var expectedSteps = "\"steps\":[{";
+ var plan = new Plan(goal, new Plan("Write a poem or joke"), new Plan("Send it in an e-mail to Kai"));
+
+ // Act
+ var serializedPlan = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan);
+ Assert.NotEmpty(serializedPlan);
+ Assert.Contains($"\"description\":\"{goal}\"", serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains($"\"description\":\"Write a poem or joke\"", serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains($"\"description\":\"Send it in an e-mail to Kai\"", serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CanSerializePlanWithPlanStep()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var stepOutput = "Output: The input was: ";
+ var expectedSteps = "\"steps\":[{";
+ var plan = new Plan(goal);
+
+ // Arrange Mocks
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
+ .Returns(() => Task.FromResult(returnContext));
+
+ plan.AddSteps(new Plan(mockFunction.Object));
+
+ // Act
+ var serializedPlan = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan);
+ Assert.NotEmpty(serializedPlan);
+ Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CanSerializePlanWithFunctionStep()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var stepOutput = "Output: The input was: ";
+ var expectedSteps = "\"steps\":[{";
+ var plan = new Plan(goal);
+
+ // Arrange
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
+ .Returns(() => Task.FromResult(returnContext));
+
+ plan.AddSteps(mockFunction.Object);
+
+ // Act
+ var serializedPlan = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan);
+ Assert.NotEmpty(serializedPlan);
+ Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CanSerializePlanWithFunctionSteps()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var stepOutput = "Output: The input was: ";
+ var expectedSteps = "\"steps\":[{";
+ var plan = new Plan(goal);
+
+ // Arrange
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
+ .Returns(() => Task.FromResult(returnContext));
+
+ plan.AddSteps(mockFunction.Object, mockFunction.Object);
+
+ // Act
+ var serializedPlan = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan);
+ Assert.NotEmpty(serializedPlan);
+ Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CanSerializePlanWithStepsAndFunction()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var stepOutput = "Output: The input was: ";
+ var expectedSteps = "\"steps\":[{";
+ var plan = new Plan(goal);
+
+ // Arrange
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
+ .Returns(() => Task.FromResult(returnContext));
+
+ plan.AddSteps(new Plan(mockFunction.Object), mockFunction.Object);
+
+ // Act
+ var serializedPlan = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan);
+ Assert.NotEmpty(serializedPlan);
+ Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CanSerializePlanWithSteps()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var stepOutput = "Output: The input was: ";
+ var plan = new Plan(goal);
+
+ // Arrange
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
+ .Returns(() => Task.FromResult(returnContext));
+
+ plan.AddSteps(new Plan(mockFunction.Object), new Plan(mockFunction.Object));
+
+ // Act
+ var serializedPlan = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan);
+ Assert.NotEmpty(serializedPlan);
+ }
+
+ [Fact]
+ public async Task CanStepAndSerializePlanWithStepsAsync()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var planInput = "Some input";
+ var stepOutput = "Output: The input was: ";
+ var plan = new Plan(goal);
+
+ // Arrange
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
+ .Returns(() => Task.FromResult(returnContext));
+
+ plan.AddSteps(mockFunction.Object, mockFunction.Object);
+
+ var serializedPlan1 = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan1);
+ Assert.NotEmpty(serializedPlan1);
+ Assert.Contains("\"next_step_index\":0", serializedPlan1, StringComparison.OrdinalIgnoreCase);
+
+ var result = await kernel.Object.StepAsync(planInput, plan);
+
+ // Act
+ var serializedPlan2 = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan2);
+ Assert.NotEmpty(serializedPlan2);
+ Assert.NotEqual(serializedPlan1, serializedPlan2);
+ Assert.Contains("\"next_step_index\":1", serializedPlan2, StringComparison.OrdinalIgnoreCase);
+
+ result = await kernel.Object.StepAsync(result);
+ var serializedPlan3 = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan3);
+ Assert.NotEmpty(serializedPlan3);
+ Assert.NotEqual(serializedPlan1, serializedPlan3);
+ Assert.NotEqual(serializedPlan2, serializedPlan3);
+ Assert.Contains("\"next_step_index\":2", serializedPlan3, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task CanStepAndSerializePlanWithStepsAndContextAsync()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var planInput = "Some input";
+ var stepOutput = "Output: The input was: ";
+ var plan = new Plan(goal);
+
+ // Arrange
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ {
+ c.Variables.Get("variables", out var v);
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input + v);
+ })
+ .Returns(() => Task.FromResult(returnContext));
+ mockFunction.Setup(x => x.Describe()).Returns(new FunctionView()
+ {
+ Parameters = new List()
+ {
+ new ParameterView() { Name = "variables" }
+ }
+ });
+
+ plan.AddSteps(mockFunction.Object, mockFunction.Object);
+
+ var cv = new ContextVariables(planInput);
+ cv.Set("variables", "foo");
+ plan = await kernel.Object.StepAsync(cv, plan);
+
+ // Act
+ var serializedPlan1 = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan1);
+ Assert.NotEmpty(serializedPlan1);
+ Assert.Contains("\"next_step_index\":1", serializedPlan1, StringComparison.OrdinalIgnoreCase);
+
+ // Act
+ cv.Set("variables", "bar");
+ cv.Update(string.Empty);
+ plan = await kernel.Object.StepAsync(cv, plan);
+
+ // Assert
+ Assert.NotNull(plan);
+ Assert.Equal($"{stepOutput}{planInput}foo{stepOutput}{planInput}foobar", plan.State.ToString());
+ mockFunction.Verify(x => x.InvokeAsync(It.IsAny(), null, null, null), Times.Exactly(2));
+
+ // Act
+ var serializedPlan2 = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan2);
+ Assert.NotEmpty(serializedPlan2);
+ Assert.NotEqual(serializedPlan1, serializedPlan2);
+ Assert.Contains("\"next_step_index\":2", serializedPlan2, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task CanStepAndSerializeAndDeserializePlanWithStepsAndContextAsync()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var planInput = "Some input";
+ var stepOutput = "Output: The input was: ";
+ var plan = new Plan(goal);
+
+ // Arrange
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ {
+ c.Variables.Get("variables", out var v);
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input + v);
+ })
+ .Returns(() => Task.FromResult(returnContext));
+ mockFunction.Setup(x => x.Describe()).Returns(new FunctionView()
+ {
+ Parameters = new List()
+ {
+ new ParameterView() { Name = "variables" }
+ }
+ });
+
+ skills.Setup(x => x.HasNativeFunction(It.IsAny(), It.IsAny())).Returns(true);
+ skills.Setup(x => x.GetNativeFunction(It.IsAny(), It.IsAny())).Returns(mockFunction.Object);
+
+ plan.AddSteps(mockFunction.Object, mockFunction.Object);
+
+ var serializedPlan = plan.ToJson();
+
+ var cv = new ContextVariables(planInput);
+ cv.Set("variables", "foo");
+ plan = await kernel.Object.StepAsync(cv, plan);
+
+ // Act
+ var serializedPlan1 = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan1);
+ Assert.NotEmpty(serializedPlan1);
+ Assert.NotEqual(serializedPlan, serializedPlan1);
+ Assert.Contains("\"next_step_index\":1", serializedPlan1, StringComparison.OrdinalIgnoreCase);
+
+ // Act
+ cv.Set("variables", "bar");
+ cv.Update(string.Empty);
+ var nextContext = new SKContext(
+ new ContextVariables(),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+ plan = Plan.FromJson(serializedPlan1, nextContext);
+ plan = await kernel.Object.StepAsync(cv, plan);
+
+ // Assert
+ Assert.NotNull(plan);
+ Assert.Equal($"{stepOutput}{planInput}foo{stepOutput}{planInput}foobar", plan.State.ToString());
+ mockFunction.Verify(x => x.InvokeAsync(It.IsAny(), null, null, null), Times.Exactly(2));
+
+ // Act
+ var serializedPlan2 = plan.ToJson();
+
+ // Assert
+ Assert.NotNull(serializedPlan2);
+ Assert.NotEmpty(serializedPlan2);
+ Assert.NotEqual(serializedPlan1, serializedPlan2);
+ Assert.Contains("\"next_step_index\":2", serializedPlan2, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CanDeserializePlan()
+ {
+ // Arrange
+ var goal = "Write a poem or joke and send it in an e-mail to Kai.";
+ var stepOutput = "Output: The input was: ";
+ var plan = new Plan(goal);
+
+ // Arrange
+ var kernel = new Mock();
+ var log = new Mock();
+ var memory = new Mock();
+ var skills = new Mock();
+
+ var returnContext = new SKContext(
+ new ContextVariables(stepOutput),
+ memory.Object,
+ skills.Object,
+ log.Object
+ );
+
+ var mockFunction = new Mock();
+ mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, null, null))
+ .Callback((c, s, l, ct) =>
+ returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
+ .Returns(() => Task.FromResult(returnContext));
+
+ plan.AddSteps(new Plan("Step1", mockFunction.Object), mockFunction.Object);
+
+ // Act
+ var serializedPlan = plan.ToJson();
+ var deserializedPlan = Plan.FromJson(serializedPlan, returnContext);
+
+ // Assert
+ Assert.NotNull(deserializedPlan);
+ Assert.Equal(goal, deserializedPlan.Description);
+
+ Assert.Equal(string.Join(",", plan.NamedOutputs.Select(kv => $"{kv.Key}:{kv.Value}")),
+ string.Join(",", deserializedPlan.NamedOutputs.Select(kv => $"{kv.Key}:{kv.Value}")));
+ Assert.Equal(string.Join(",", plan.NamedParameters.Select(kv => $"{kv.Key}:{kv.Value}")),
+ string.Join(",", deserializedPlan.NamedParameters.Select(kv => $"{kv.Key}:{kv.Value}")));
+ Assert.Equal(string.Join(",", plan.State.Select(kv => $"{kv.Key}:{kv.Value}")),
+ string.Join(",", deserializedPlan.State.Select(kv => $"{kv.Key}:{kv.Value}")));
+
+ Assert.Equal(plan.Steps[0].Name, deserializedPlan.Steps[0].Name);
+ Assert.Equal(plan.Steps[1].Name, deserializedPlan.Steps[1].Name);
+ }
+}
diff --git a/dotnet/src/SemanticKernel/Orchestration/ContextVariablesConverter.cs b/dotnet/src/SemanticKernel/Orchestration/ContextVariablesConverter.cs
index f60e58abae..ca03d1047d 100644
--- a/dotnet/src/SemanticKernel/Orchestration/ContextVariablesConverter.cs
+++ b/dotnet/src/SemanticKernel/Orchestration/ContextVariablesConverter.cs
@@ -16,12 +16,11 @@ public class ContextVariablesConverter : JsonConverter
/// Read the JSON and convert to ContextVariables
///
/// The JSON reader.
- /// The type to convert from.
+ /// The type to convert.
/// The JSON serializer options.
/// The deserialized .
public override ContextVariables Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- // Needs Test
var keyValuePairs = JsonSerializer.Deserialize>>(ref reader, options);
var context = new ContextVariables();
@@ -35,7 +34,16 @@ public class ContextVariablesConverter : JsonConverter
public override void Write(Utf8JsonWriter writer, ContextVariables value, JsonSerializerOptions options)
{
- // Needs Test
- JsonSerializer.Serialize(writer, value, options);
+ writer.WriteStartArray();
+
+ foreach (var kvp in value)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("Key", kvp.Key);
+ writer.WriteString("Value", kvp.Value);
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndArray();
}
}
diff --git a/dotnet/src/SemanticKernel/Planning/Plan.cs b/dotnet/src/SemanticKernel/Planning/Plan.cs
index 7e03210d7a..b29a573491 100644
--- a/dotnet/src/SemanticKernel/Planning/Plan.cs
+++ b/dotnet/src/SemanticKernel/Planning/Plan.cs
@@ -159,12 +159,15 @@ public sealed class Plan : ISKFunction
/// JSON string representation of a Plan
/// The context to use for function registrations.
/// An instance of a Plan object.
- public static Plan FromJson(string json, SKContext context)
+ /// If Context is not supplied, plan will not be able to execute.
+ public static Plan FromJson(string json, SKContext? context = null)
{
- // Needs Test
var plan = JsonSerializer.Deserialize(json, new JsonSerializerOptions() { IncludeFields = true }) ?? new Plan(string.Empty);
- plan = SetRegisteredFunctions(plan, context);
+ if (context != null)
+ {
+ plan = SetRegisteredFunctions(plan, context);
+ }
return plan;
}
@@ -174,7 +177,6 @@ public sealed class Plan : ISKFunction
///
public string ToJson()
{
- // Needs Test
return JsonSerializer.Serialize(this);
}
@@ -449,7 +451,6 @@ public sealed class Plan : ISKFunction
}
else if (this.State.Get(param.Name, out value) && !string.IsNullOrEmpty(value))
{
- // Needs test
stepVariables.Set(param.Name, value);
}
}