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); } }